Spark优雅的操作Redis

Spark的优势在于内存计算,然而在计算中难免会用到一些元数据或中间数据,有的存在关系型数据库中,有的存在HDFS上,有的存在HBase中,但其读写速度都和Spark计算的速度相差甚远,而Redis基于内存的读写则可以完美解决此类问题,下面介绍Spark如何与Redis交互。
在Spark计算的时候如何加载Redis中的数据,其实官方有现成的包和文档,文档是全英文,好在东西不多,下面介绍如何使用。

首先把jar包引入工程,在maven上居然找不到这个包。。。所以使用Maven和SBT的同学自行解决。下载地址(打不开的同学可以尝试翻墙)

2018-11-06更新: 最新maven依赖已经被中央仓库收录([https://github.com/RedisLabs/spark-redis](https://github.com/RedisLabs/spark-redis)
)
<dependency>
    <groupId>RedisLabs</groupId>
    <artifactId>spark-redis</artifactId>
    <version>0.3.2</version>
</dependency>

可以看出提供的功能还是挺全面的,有单独的redis分区,redisRDD,SQLAPI以及StreamingAPI

下面我们一点一点来做一个示例:

在这里先看看官方包中的一部分源码:

/**
    官方提供源码包中解析Redis配置需要的字段
*/
case class RedisEndpoint(val host: String = Protocol.DEFAULT_HOST,
                         val port: Int = Protocol.DEFAULT_PORT,
                         val auth: String = null,
                         val dbNum: Int = Protocol.DEFAULT_DATABASE,
                         val timeout: Int = Protocol.DEFAULT_TIMEOUT)
  extends Serializable {

/**
    源码中获取配置的字段名及来源,可以看出是从SparkConf中读取到相应字段,所以连接redis只需在SparkConf中set相应字段即可
*/
  def this(conf: SparkConf) {
      this(
        conf.get("redis.host", Protocol.DEFAULT_HOST),
        conf.getInt("redis.port", Protocol.DEFAULT_PORT),
        conf.get("redis.auth", null),
        conf.getInt("redis.db", Protocol.DEFAULT_DATABASE),
        conf.getInt("redis.timeout", Protocol.DEFAULT_TIMEOUT)
      )
  }

  ···
}

现在我们启动SparkContext

先引入Redis相关的隐式转换
import com.redislabs.provider.redis._

//这里直接使用yarn-cluster模式
val conf = new SparkConf().setMaster("yarn-cluster").setAppName("sparkRedisTest")
conf.set("redis.host", "10.1.11.70")    //host,随便一个节点,自动发现
conf.set("redis.port", "6379")  //端口号,不填默认为6379
//conf.set("redis.auth","null")  //用户权限配置
//conf.set("redis.db","0")  //数据库设置
//conf.set("redis.timeout","2000")  //设置连接超时时间
val sc = new SparkContext(conf)

之后可以看到IDEA给出的提示,sc通过导入的隐式转换可以调出的读取Redis的方法,都是以fromRedis开头的,都是redis可以存储的数据结构,这里以常见的KV进行示例


还是先扒一下源码看看:

def fromRedisKV[T](keysOrKeyPattern: T,
                     partitionNum: Int = 3)
                    (implicit redisConfig: RedisConfig = new RedisConfig(new RedisEndpoint(sc.getConf))):
  RDD[(String, String)] = {
    keysOrKeyPattern match {
      case keyPattern: String => fromRedisKeyPattern(keyPattern, partitionNum)(redisConfig).getKV
      case keys: Array[String] => fromRedisKeys(keys, partitionNum)(redisConfig).getKV
      case _ => throw new scala.Exception("KeysOrKeyPattern should be String or Array[String]")
    }
  }

先看传入的参数:

  1. 泛型类型keysOrKeyPattern
    从的模式匹配代码中可以看出,这里的T可是是两种类型,一个是String,另一个是Array[String],如果传入其他类型则会抛出运行时异常,其中String类型的意思是匹配键,这里可以用通配符比如foo*,所以返回值是一个结果集RDD[(String, String)],当参数类型为Array[String]时是指传入key的数组,返回的结果则为相应的的结果集,RDD的内容类型也是KV形式。
  2. Int类型partitionNum
    生成RDD的分区数,默认为3,如果传入的第一个参数类型是Array[String],这个参数可以这样设置,先预估一下返回结果集的大小,使用keyArr.length / num + 1,这样则保证分区的合理性,以防发生数据倾斜。若第一个参数类型为String,能预估尽量预估,如果实在没办法,比如确实在这里发生了数据倾斜,可以尝试考虑使用sc.fromRedisKeys()返回key的集合,提前把握返回结果集的大小,或者根据集群机器数量,把握分区数。
  3. 柯里化形式隐式参数redisConfig
    由于我们之前在sparkConf里面set了相应的参数,这里不传入这个参数即可。如要调整,则可以按照源码中的方式传入,其中RedisEndpoint是一个case class类,而且很多参数都有默认值(比如6379的端口号),所以自己建立一个RedisEndpoint也是非常方便的。
    了解了参数之后来继续完成测试代码:
/*这里标出了resultSet的类型*/
val resultSet:RDD[(String, String)] = sc.fromRedisKV("to*")
//找出键以`to`开头的键值对,这里就不进行计算了,直接保存到HDFS看结果如何,同时合并分区便于观察结果
resultSet.coalesce(1).saveAsTextFile("HDFSpath")

现在往redis里面随便set几个数据

Redis shell

打包之后运行,命令为:

spark-submit --master yarn --deploy-mode cluster --class test.SparkRedis --jars jedis-2.9.0.jar,spark-redis-0.3.2.jar,/opt/cloudera/parcels/CDH/jars/commons-pool2-2.2.jar --driver-class-path jedis-2.9.0.jar,spark-redis-0.3.2.jar,/opt/cloudera/parcels/CDH/jars/commons-pool2-2.2.jar spark-redis.jar

命令中指明了依赖的资源包:jedis-2.9.0.jar,spark-redis-0.3.2.jar,commons-pool2-2.2.jar其中commons-pool2-2.2.jar是spark-redis依赖的包,如果集群环境为CDH发行版,可在/opt/cloudera/parcels/CDH/jars/commons-pool2-2.2.jar下找到该包,,而且yarn的运行环境里面没有默认引入该包;如果为自建环境,则需要自行下载该包,Maven上搜索commons-pool2即可。

等待执行成功


执行UI

再看一下DAG图

DAG图

运行结果


运行结果

和预期的一样,以to开头的数据都被找到。

如果传入的是key数组

val keys = Array[String]("high", "abc", "together")
sc.fromRedisKV(keys).coalesce(1).saveAsTextFile("hdfs://nameservice1/spark/test/redisResult2")

结果如下:

运行结果

完整代码:

import org.apache.spark.{SparkConf, SparkContext}
import com.redislabs.provider.redis._

object SparkRedis extends App {
  val conf = new SparkConf().setMaster("yarn-cluster").setAppName("sparkRedisTest")
  conf.set("redis.host", "10.1.11.70")
  val sc = new SparkContext(conf)
  val keys = Array[String]("high", "abc", "together")
  sc.fromRedisKV(keys).coalesce(1).saveAsTextFile("hdfs://nameservice1/spark/test/redisResult2")
}

下面看如何写入Redis


还是以常见的KV为例
源码中是这样处理的,接收两个参数,RDD类型为RDD[(String, String),第二个为失效时间

def toRedisKV(kvs: RDD[(String, String)], ttl: Int = 0)
               (implicit redisConfig: RedisConfig = new RedisConfig(new RedisEndpoint(sc.getConf))) {
    kvs.foreachPartition(partition => setKVs(partition, ttl, redisConfig))
  }

测试代码为:

val data = Seq[(String,String)](("high","111"), ("abc","222"), ("together","333"))
val redisData:RDD[(String,String)] = sc.parallelize(data)
sc.toRedisKV(redisData)

先清空一下redis


redis shell

打包后按相同的命令提交到集群并执行成功后即可看到数据

redis shell

完整代码:

import org.apache.spark.{SparkConf, SparkContext}
import com.redislabs.provider.redis._
import org.apache.spark.rdd.RDD

object SparkRedis extends App {
  val conf = new SparkConf().setMaster("yarn-cluster").setAppName("sparkRedisTest")
  conf.set("redis.host", "10.1.11.70")
  val sc = new SparkContext(conf)
  val data = Seq[(String,String)](("high","111"), ("abc","222"), ("together","333"))
  val redisData:RDD[(String,String)] = sc.parallelize(data)
  sc.toRedisKV(redisData)
}

原创文章@贪恋清晨de阳光

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容