背景
某项目引入了一个新需求,需要根据某数据表的数据进行数据处理,这个数据存储在哪里呢?内存 VS 缓存组件呢?权衡之下选择使用 Redis 缓存。
调研 Java 操作 Redis 的客户端时,主要有 Jedis 和 lettuce 两种,而且各种资料显示后者比前者应用范围更广,效率更高,且是 spring-boot 生态默认支持的。
最开始我也是直接调研 lettuce 的用法的,部署测试时发现引用的 lettuce 依赖的 netty 版本和已有的 netty 版本不匹配,又继续调研 Jedis 。
lettuce 调研
lettuce 最新版本6.5.5 依赖的 netty 是4.1.65,我们那个老项目应用的 netty-all ,最后一次更新是在10年前,版本太低了,兼容不了 lettuce ,其中一些新版本的方法都没有,运行时会报异常。
Jedis 调研
Jedis 倒是一直在更新,依赖的包比较少,项目中都有,而且 json 这个包即使没有,API 也不会报错,可见它不是必须的。 此外,它依赖了连接池包 commons-pool2,注意,低版本的 jedis-2.9.0 依赖的连接池版本也低,高版本的连接池的构造函数语法有变动,需要匹配对应版本。
基于Jedis 的 Redis 操作
Redis 支持三种部署方式:单机、集群和哨兵。那么我们在编写 Redis 操作的时候就要考虑三种类型的客户端,通用的方法是使用连接池。这里可以简单实用工厂模式来实现 Redis 客户端对象的获取,定义顶层抽象类,包含公共的 Redis 的操作方法:
ping()
get(String key)
set(String key, Object value)
setBatch(List<Map> data)
del(String key)
子类实现按三类部署方式对应的客户端API来处理。
第一类,单机连接池和批处理操作:
c
// Part1:创建连接池对象
JedisPool jedisPool = new JedisPool(config, url, port, timeout, password, databaseIndex)
// Part2:批量写入,使用 Pipeline 对象
public void setBatch(Map<String, String> datas) {
try (Jedis jedis = jedisPool.getResource()) {
Pipeline pipelined = jedis.pipelined();
datas.forEach((key, value) ->{
pipelined.set(key, value);
});
// 一批次刷新
pipelined.sync();
// 必须关闭否则会资源泄漏
pipelined.close();
} catch (Exception e) {
logger.error("Redis set data error", e);
}
}
第二类,集群连接池和批处理操作:
c
// Part1:创建连接池对象
Set<HostAndPort> jedisClusterNodes = new HashSet<>();
// TODO 添加集群节点到 jedisClusterNodes
// 创建集群连接池对象
JedisCluster jedisCluster = new JedisCluster(jedisClusterNodes,
connectTimeout,
timeout,
maxRetryCount,
password,
poolConfig);
// Part2:批量写入,使用 Pipeline 对象
public void setBatch(Map<String, String> datas) {
try (ClusterPipeline pipelined = jedisCluster.pipelined()) {
datas.forEach((key, value) ->{
pipelined.set(key, value);
});
// 一批次刷新
pipelined.sync();
} catch (Exception e) {
logger.error("Redis set data error", e);
}
}
第三类,哨兵连接池操作和批处理操作:
c
// Part1:创建连接池对象
Set<String> sentinels = new HashSet<>(Arrays.asList(urls.split(",")));
JedisSentinelPool jedisPool = new JedisSentinelPool(masterName, sentinels,
poolConfig,
timeout,
password,
databaseIndex);
// Part2:批量写入,使用 Pipeline 对象
public void setBatch(Map<String, String> datas) {
try (Jedis jedis = jedisPool.getResource()) {
Pipeline pipelined = jedis.pipelined();
datas.forEach((key, value) ->{
pipelined.set(key, value);
});
// 一批次刷新
pipelined.sync();
// 必须关闭否则会资源泄漏
pipelined.close();
} catch (Exception e) {
logger.error("Redis set data error", e);
}
}
启示录
解决这个需求后,我想到一个问题技术框架也有高低优劣吗?Jedis 这个短小精悍的工具包,而且一直在保持更新,依赖少,新版本提供了 Pipeline 这个批处理的方法,可以将一批操作封装在一起一次提交,效率非常高。
我测试了50万条数据写入 Redis,用 Pipeline 后可以在5秒内完成,真正能达到10万每秒。但是如果单独一条条发送,新版本「5.2.0」的效率反而低于低版本「2.9.0」,集群效率又低于单机效率,高版本的单机在150秒左右,高版本的集群在 600 秒左右。
版本更迭,依赖变化,保持最小依赖不容易啊!