spark使用redis的連接池

這幾天碰到了類似的問題, 網上查的一些資料, 這裏記錄一下~

1. Master

將所有的數據全部回收到master, 然後在master進行集中處理

連接池代碼:

public class TestRedisPool {
	private JedisPool pool = null;
	public TestRedisPool(String ip, int port, String passwd, int database) {
		if (pool == null) {
			JedisPoolConfig config = new JedisPoolConfig();
			config.setMaxTotal(500);
			config.setMaxIdle(30);
			config.setMinIdle(5);
			config.setMaxWaitMillis(1000 * 10);
			config.setTestWhileIdle(false);
			config.setTestOnBorrow(false);
			config.setTestOnReturn(false);
			pool = new JedisPool(config, ip, port, 10000, passwd, database);
			Logs.debug("init:" + pool);
		}
	}
	public JedisPool getRedisPool() {
		return pool;
	}
	public String set(String key,String value){
		Jedis jedis = null;
		try {
			jedis = pool.getResource();
			return jedis.set(key, value);
		} catch (Exception e) {
			e.printStackTrace();
			return "0";
		} finally {
			jedis.close();
		}
	}
}

使用方式:

List<String> list = Arrays.asList("a","b","c","d", "e");
JavaRDD<String> javaRDD = new JavaSparkContext(spark.sparkContext()).parallelize(list, 3);
TestRedisPool testRedisPool = new TestRedisPool(redisIp, port, passwd, dbNum);
List<String> lst = javaRDD.collect();
for(String s:lst) {
	testRedisPool.set(s, getDateString2(0));
}

2. Worker

在worker遍歷的時候初始化連接池

javaRDD.foreach(new VoidFunction<String>() {
	@Override
	public void call(String s) throws Exception {
		TestRedisPool testRedisPool = new TestRedisPool(redisIp, port, passwd, dbNum);
		Logs.debug(testRedisPool.getRedisPool());
		testRedisPool.set(s, getDateString2(0));
	}
});

遍歷所有元素,TestRedisPool不需要實現序列化;每一個RDD中的元素都需要創建很多的redis連接池,即便使用短連接也會對redis造成很大的壓力。效率也是極其低下的。

3. Master上創建,Worker上遍歷

在Master上創建一個實例,在進行分區遍歷時使用Master上創建的實例,這種方式是可以的,只需要將類實現序列即可。同時還可以通過廣播變量,將實例在Worker上持久化,減少實例使用時的網絡傳輸。

TestRedisPool testRedisPool = new TestRedisPool(redisIp, port, passwd, dbNum);
javaRDD.foreach(new VoidFunction<String>() {
	@Override
	public void call(String s) throws Exception {
		Logs.debug(testRedisPool.getRedisPool());
		testRedisPool.set(s, getDateString2(0));
	}
});
Exception in thread "main" org.apache.spark.SparkException: Task not serializable
...
Serialization stack:
	- object not serializable (class: redis.clients.jedis.JedisPool, value: redis.clients.jedis.JedisPool@3e4f80cb)

報錯jedispool無法序列化,即使TestRedisPool類實現了序列化,但因爲其成員變量jedispool本身並不支持序列化,所以這種方式在有成員變量無法序列化時也不可用。

4. Worker上按分區遍歷

javaRDD.foreachPartition(new VoidFunction<Iterator<String>>() {
	@Override
	public void call(Iterator<String> stringIterator) throws Exception {
		TestRedisPool testRedisPool = new TestRedisPool(redisIp, port, passwd, dbNum);
		while (stringIterator.hasNext()) {
			Logs.debug(testRedisPool.getRedisPool());
			testRedisPool.set(stringIterator.next(), getDateString2(0));
		}
	}
});

TestRedisPool不需要實現序列化,每個分區只需要創建一個redis連接池

5. 使用靜態類型,按分區遍歷

在上面,我們可以做到在每個分區上建立連接池,但是每臺機器一般對應多個分區,怎麼進一步減少連接池的創建呢。我們知道靜態類型全局只有一份,如果將redis連接池定義爲靜態類型,做到每個worker上只創建一個redis連接池。

public class TestRedisPool {
	private static JedisPool pool = null;
	...
}

錯誤使用:

TestRedisPool testRedisPool = new TestRedisPool(redisIp, port, passwd, dbNum);
javaRDD.foreachPartition(new VoidFunction<Iterator<String>>() {
	@Override
	public void call(Iterator<String> stringIterator) throws Exception {
		while (stringIterator.hasNext()) {
			Logs.debug(testRedisPool.getRedisPool());
			testRedisPool.set(stringIterator.next(), getDateString2(0));
		}
	}
});

這種在Master上創建TestRedisPool實例的方式,在worker上無法獲取到,會報java.lang.NullPointerException異常。

正確使用:

javaRDD.foreachPartition(new VoidFunction<Iterator<String>>() {
	@Override
	public void call(Iterator<String> stringIterator) throws Exception {
		TestRedisPool testRedisPool = new TestRedisPool(redisIp, port, passwd, dbNum);
		while (stringIterator.hasNext()) {
			Logs.debug(testRedisPool.getRedisPool());
			testRedisPool.set(stringIterator.next(), getDateString2(0));
		}
	}
});

TestRedisPool也不需要序列化。這種情況下是在分區上分別創建實例,分區對應的就是虛擬線程的個數,所以相當於3個線程同時去獲取jedispool實現,所以一共init了三次。如果做成單例模式就能解決init多次的問題。

6. 使用單例模式,按分區遍歷

連接池代碼:

package com.project.uitl;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Redis 連接池工具包
 *
 */
public class JedisPoolUtil {
    
    private static final String HOST = "132.232.6.208";
    private static final int PORT = 6381;
    
    private static volatile JedisPool jedisPool = null;
    
    private JedisPoolUtil() {}
    
    /**
     * 獲取RedisPool實例(單例)
     * @return RedisPool實例
     */
    public static JedisPool getJedisPoolInstance() {
        if (jedisPool == null) {
            synchronized (JedisPoolUtil.class) {
                if (jedisPool == null) {
                    
                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    poolConfig.setMaxTotal(1000);           // 最大連接數
                    poolConfig.setMaxIdle(32);              // 最大空閒連接數
                    poolConfig.setMaxWaitMillis(100*1000);  // 最大等待時間
                    poolConfig.setTestOnBorrow(true);       // 檢查連接可用性, 確保獲取的redis實例可用
                    
                    jedisPool = new JedisPool(poolConfig, HOST, PORT);
                }
            }
        }
        
        return jedisPool;
    }
    
    /**
     * 從連接池中獲取一個 Jedis 實例(連接)
     * @return Jedis 實例
     */
    public static Jedis getJedisInstance() {
        
        return getJedisPoolInstance().getResource();
    }
    
    /**
     * 將Jedis對象(連接)歸還連接池
     * @param jedisPool 連接池
     * @param jedis 連接對象
     */
    public static void release(JedisPool jedisPool, Jedis jedis) {
        
        if (jedis != null) {
            jedisPool.returnResourceObject(jedis);  // 已廢棄,推薦使用jedis.close()方法
        }
    }
}

以上volatile保證當jedispool未初始化完成是不能被獲取到,synchronized解決多線程衝突的問題。這兩個關鍵詞的使用其實也就是lazy initialize的實現。

javaRDD.foreachPartition(new VoidFunction<Iterator<String>>() {
	@Override
	public void call(Iterator<String> stringIterator) throws Exception {
		TestRedisPool testRedisPool = new TestRedisPool(redisIp, port, passwd, dbNum);
		while (stringIterator.hasNext()) {
			Logs.debug("class:" + testRedisPool );
			Logs.debug("pool:" + testRedisPool .getRedisPool());
			testRedisPool .set(stringIterator.next(), getDateString2(0));
		}
	}
});

現在jedispool只init了一次,並且全局也只有一個jedispool。但是現在TestRedisPool對象還是被創建了多個,改爲在Master上定義,並已廣播變量的形式分發到Worker上可以解決這個問題,這種情況下TestRedisPool需要序列化。

7. 使用單例模式,Driver上定義,分區上遍歷

TestRedisPool testRedisPool = new TestRedisPool(redisIp, port, passwd, dbNum);
final Broadcast<TestRedisPool> broadcastRedis = new JavaSparkContext(spark.sparkContext()).broadcast(testRedisPool);
javaRDD.foreachPartition(new VoidFunction<Iterator<String>>() {
	@Override
	public void call(Iterator<String> stringIterator) throws Exception {
		TestRedisPool redisClient1 = broadcastRedis.getValue();
		while (stringIterator.hasNext()) {
			Logs.debug("class:" + redisClient1);
			Logs.debug("pool:" + redisClient1.getRedisPool());
			redisClient1.set(stringIterator.next(), getDateString2(0));
		}
	}
});

現在是TestRedisPool在Master上定義,廣播到各個Worker上;同時jedispool在每臺worker上也始終只會有一個實例存在。

但是也會有人會疑問,爲什麼jedispool現在沒有序列化的問題(方法三),或者定義成靜態導致worker上獲取不到jedispool(方法五第一種情況)的問題。這是因爲方法三中jedispool爲普通類型是和類一起序列化,因爲其本身不支持序列化,所以報錯 方法五中,定義成靜態類型之後,靜態類型不屬於類,所以TestRedisPool序列化不會出錯,但是因爲jedispool在Master上定義和初始化,不會傳輸到節點上,節點上獲取到的jedispool都爲null,所以報錯。而方法七中使用懶啓動的方式,在使用時纔會初始化jedispool,所以實際是在節點上完成的初始化,所以不會有問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章