ITPub博客

首页 > 数据库 > 数据库开发技术 > Spark中使用Threadlocal,解决单例模式的尴尬

Spark中使用Threadlocal,解决单例模式的尴尬

原创 数据库开发技术 作者:安瑞哥是码农 时间:2024-07-09 20:53:37 0 删除 编辑


本文是继(Spark 要引入外部数据源,咋整?——(中))的下篇,为了不让你对标题产生审美疲劳,我特意换了个名字。


熟悉「ThreadLocal」的同学应该都知道,这个知识点在纯 Java 界,已经被各种文章讲烂了,但是,这玩意跟 Spark 的结合,却鲜有文章提及。


那今天这篇内容呢,就结合我的实践经历,来跟你聊聊,Spark 怎么样用它,来优雅地解决外部数据源 Redis(其他数据库也一样) 的引入问题。



0. 为什么不能放开连接


前面文章提到,为了降低 Redis 的连接管理成本,于是我们考虑使用「连接单例」,但是如果直接只使用这个连接单例,就会导致在「多线程」情况下,对单个连接的争抢问题(Spark 如果要引入外部数据源,咋整?——(上))。


这个时候,有同学可能觉得,那我根据线程需要来 new 不就好了吗,比如像下面这种:


.foreachPartition(iter => {
                val connection = new RedisConnection()
                while (iter.hasNext){
                    connection...
                    //todo
                }
              }

这样写,确实可以解决问题。


但是,如果在团队中,万一有些水平比较蹩脚的同学,手一哆嗦,就给写成了下面这样:


.foreach(data => {
                val connection = new RedisConnection()
                connection...
                //todo
            })

就问你慌不慌?


根据我的经验,但凡像这种需要你很小心去注意的代码逻辑,一定要把它封装成「工具类」,供团队小伙伴统一使用,否则,你把这些地方的自主权交给个人,指不定会冒出什么样的牛鬼蛇神,分分钟把你的数据库给干崩掉。



1. ThreadLocal 能解决什么


既然连接单例有缺点,而完全放开连接管理给开发的同学,又有失控的风险(就算用线程池,也一样有这个风险)。


那有没有一种,既能控制数据库的连接个数,又可以完全满足多线程情况下,对连接数量的动态需求呢?


有!连接单例 + ThreadLocal 组合。


关于 ThreadLocal 的概念,网上的资料已经烂大街了,这里不赘述,我只想告诉你,它在当前场景里解决问题是:我只需要在一个进程中 new 一个连接对象,然后通过 ThreadLocal 给「按需」复制到各个线程里面,让每个线程都可以拥有属于自己的那份连接。


这里最妙的地方,就在于「按需」这两个字,在代码实现上,你虽然只 new 了一个连接,但如果你在启动任务时,想让每个 executor 并行多跑几个 partition 的数据,这个时候,它就可以根据每个 executor 并行的 partition 数量,自动来创建等量的数据库连接对象。


来,看一眼 Redis 连接单例,跟 ThreadLocal 组合的核心代码:


定义全局的连接对象单例


获取装有 Redis 连接对象的 ThreadLocal 对象



2. 正式代码


Spark 的主体代码如下:


package com.anryg.bigdata.test.variable

import com.anryg.bigdata.RedisClientUtilsV2
import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession

/**
  * @DESC: 测试Spark利用Redis的连接单例 + ThreadLocal来引入外部数据源
  * @Auther: Anryg
  * @Date: 2024/7/9 17:45
  */

object SparkThreadLocalTest03 {

    def main(args: Array[String]): Unit = {
        val sparkConf = new SparkConf().setAppName("SparkThreadLocalTest03")
        val spark = SparkSession.builder().config(sparkConf).getOrCreate()

        /**构造数据源*/
        val seq = Seq("baidu.com",
            "qq.com",
            "xiaomi.com",
            "12345.com",
            "abc.com",
            "def.net",
            "xxx.com",
            "google.com",
            "sina.com",
            "163.com",
            "yyy.net",
            "360.com",
            "cctv.cn",
            "12306.com")

        spark.sparkContext.makeRDD(seq) /**生成 RDD 对象*/
            .foreach(domain => {
                /**通过ThreadLocal获取Redis连接,并拿到黑名单数据*/
                val blackDomainSet = RedisClientUtilsV2.getSetResult(RedisClientUtilsV2.getRedisThreadLocal.get(),
                    5,
                    "black_domain")

            if (!blackDomainSet.contains(domain)) {
                Thread.sleep(5000) /**为了方便观察Redis的连接情况,sleep几秒*/
                println(domain)
            }
        })
    }
}

通过现在的这种写法,你就既不用担心 Spark 在多线程运行时,连接对象争抢的问题,也不用担心连接对象滥用的问题。


注意看这里:


有的同学看到,代码中对 Redis 连接对象的获取逻辑,是被内置在「foreach」算子中的,可能会想:我C,那这岂不是会每一条数据都要 new 一个连接对象?这连接对象不得爆了呀?


有这个怀疑的,只能说明你对「单例」的理解还不够透彻,在当前代码里,Redis 用的是连接单例,所以这里不管你用的是「foreach」还是「foreachPartition」,因为在「同一个进程内部」,所以效果都是一样的,因为它们用的都是同一个连接。


而「ThreadLocal」在这里起的作用,就是把连接对象,根据线程的个数,自动进行复制,有几个线程,就复制几份,不用担心线程间,连接争抢的问题。



3. 看运行效果


在看效果前,我们先来瞅一眼 Redis 在程序运行前的基础连接情况。


当前的基础连接个数为 6 (有其他任务长期占有了6个连接)。


3.1 线程数设置为1时


此时,Redis 的总连接数增加1个,变为 7。


3.2 线程数设置为2时



此时,Redis 的总连接数增加2个,变为 8。


3.3 线程数设置为3时



此时,Redis 的总连接数增加3个,变为 9。


3.4 线程数设置为4时



此时,Redis 的总连接数增加 4 个,变为 10。


3.5 线程数设置为5时



此时,Redis 的总连接数增加 5 个,变为 11。


3.6 线程数设置为6时



咦?怎么不是 12 个呢?开动你的脑瓜子想一想。


因为根据分区规则,第 6 个分区是空的,木有数据,所以也就不创建新的连接了。


从上面的测试结果来看,ThreadLocal 的确可以做到,随着线程数量的增加,连接数量按需增长(之前直播也演示过)。


当然,以上所有测试,黑名单的碰撞结果都没有问题,因为不是这次测试的重点,就不贴出来了。



最后


相比之前纯粹的「连接单例」模式,当前这种「连接单例 + ThreadLocal」的模式,解决了「连接争抢」的问题。


而又相比之前的「广播变量」模式,省去了从 driver 端,到 executor 端的数据传输问题。


那对于当前的这种,在 Spark 中引入外部数据源的方式,你中意否?




你可以添加我的私人微信,拉你入技术讨论群,跟一群热爱技术的小伙伴一起成长...


原文作者:安瑞哥是码农;如有侵权,请联系管理员删除。

上一篇: 啪!打脸了。
请登录后发表评论 登录
全部评论

注册时间:2022-02-16

  • 博文量
    228
  • 访问量
    115417