Redisson在项目中的实践
Redisson在项目中的实践
最近在项目中引入了 Redisson 这个组件,使用场景主要是分布式锁、信号量以及一些分布式集合等。
说是一个组件,其实本质上还是一个 Java Redis 的客户端。 不过跟 Jedis 跟 Lettuce 这两个客户端比较起来,还是有些比较明显的特点跟优势。 不然项目中也不会单独额外的引用另一个客户端组件。
1. Redisson 特点
Redisson 使用下来可以总结出下面几点好用的地方:
- 像使用普通 Java 对象一样使用 Redis。比如,你可以用使用 Map、List、Set 等集合去操作 Redis 里面的数据。操作起来特别的简单,你几乎感觉不到数据是存储在 Redis 中。
- 做分布式锁比较方便,不用考虑各种场景下的异常等。
- 支持集群、哨兵等模式。不用担心数据只支持存储在单个 Redis 节点上。
- 支持在客户端本地保存一部分数据,减少网络请求。 有点类似于多级缓存一样的。
- 跟 Spring 框架可以进行集成,减少一些配置成本。
基本上上面几点,就已经覆盖了实际项目中大部分的场景。 同时也满足一定的性能要求,不用担心 Redisson 的逻辑处理会成为瓶颈。
2. 对比 Jedis、Lettuce
Jedis 算是 Redis 官方提供的 Java 版本客户端。在众多 Java 客户端中也是最老牌的一个,从2010年左右开始发布,到现在已经很多年了。
作为官方提供的客户端,也是提供了很直观的 API 来操作 Redis 。 同时社区也是非常活跃的,一些 Bug 提上去比较快速就能被解决。
但是有些缺点比较明显:
- Jedis 操作都是同步的,意味着在等待Redis响应时,线程会被阻塞。在一些并发高的场景下性能稍微会低一点。
- Jedis 实例不是线程安全的,要在多线程环境下操作通常需要做一些锁之类的处理。
- 对异步操作支持的不太好。
Lettuce 是一个稍微新一点的 Redis Java客户端,之前应该也是关注度不是特别高。 我第一次注意到它是 Spring Data Redis 开始替换默认的实现为 Lettuce 的时候,特地去搜索了下有什么特别的地方。
Lettuce 是 2011年发布的首个版本,2014年左右开始不怎么活跃。 而且 Lettuce 的作者居然是 wrk 的作者,这倒是很让人以外。
Lettuce 是基于 Netty 框架去开发的,所以很容易猜的出来,它肯定是异步非阻塞操作。这样有利于高效的处理大量并发连接,同时 Lettuce 的连接是线程安全的,支持多个线程共享一个连接。
当然 Lettuce 也还是有很多其他的特性,不过也存在一些不足:
- Lettuce 的 API 会稍微复杂一点,因为很多异步操作以及响应式编程。
- Lettuce 的文档稍微没那么多,案例会更少。
- 涉及到 Netty ,一些 Netty 的参数也会成为性能优化的方面,给研发增加一定的负担。
对比起来,Redisson 能流行还是依靠提供了更加便利,更符合 Java 程序员一些 API 。 通过这些特性可以让开发过程中减少很大一部分工作量。
3. Redisson 基本的用法
3.1 连接 Redis
通过 Redisson 连接 Redis 的步骤还是比较少的,比如下面三行代码就连接一个本地的 Redis 实例。
final Config config=new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson=Redisson.create(config);
在实际项目开发中,通常配置都会写在配置文件中,Redisson 也支持直接从 yml 等文件中配置属性。这样可以让项目更加灵活。比如下面是官方的配置文件示例:
---
singleServerConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
password: -1
subscriptionsPerConnection: 5
clientName: null
address: "redis://1.1.1.1:1"
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 24
connectionPoolSize: 64
database: -1
dnsMonitoringInterval: 5000
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.JsonJacksonCodec> { }
transportMode: "NIO"
然后读取上面的文件初始化实例:
final InputStream fileStream=Files.newInputStream(new File("配置文件路径").toPath());
final Config redissonConfig=Config.fromYAML(fileStream);
final RedissonClient redisson=Redisson.create(redissonConfig);
3.2 分布式锁
分布式锁主要确保是多节点环境中对共享资源的安全访问。比如你在单机的 Java 应用中,为了保证对某一个变量等进行安全访问,就会进行加锁的操作。
private final Lock lock=new ReentrantLock();
lock.lock();
try{
// 加锁的逻辑
}finally{
lock.unlock();
}
但是本地锁只在单个 JVM内有效,所以只适用于单机多线程环境。
使用 Redis 做分布式锁的话一般会使用 setnx 指令去实现,或者使用 lua 脚本去实现,都比较麻烦。
import org.redisson.api.RLock;
final RLock lock=redisson.getLock("myLock");
lock.lock();
try{
// 执行需要同步的代码
}finally{
lock.unlock();
}
Redisson 提供的分布式锁跟 Java 内置的锁使用方式上几乎是一模一样,背后的工作都由 Redisson 去处理。包括锁时间的续期、集群节点下挂了几个节点导致锁出现问题等。
3.3 分布式 Map
RedissonMap 是 Redisson 提供的一个分布式数据结构。 特点就是它类似于 Java 的 ConcurrentHashMap,但是它可以在分布式环境中使用。
用着 Map 的 API ,但是它实际上操作的是 Redis 上的数据,很大程序上可以简化程序中的一些操作。
// 初始化一个 Map ,并放入 key value
final RMap<String, Integer> map=redisson.getMap("myMap");
map.put("key",1);
// 获取 map 中 key对应的 value
final Integer value=map.get("key");
System.out.println(value);
上面的例子,看着就是跟 Java 的 Map 操作一模一样。实际上在 Redis 中存储的数据是用的哈希结构。Map 的 Key Value 则存储为 Redis 中的 Hash key 和 Hash value。
3.3 分布式队列 Queue
分布式队列是类似于 Java 中的 Queue 接口,但是它是支持在分布式环境中使用的队列数据结构。它利用 Redis 来实现跨多个节点的队列功能,利用 Redis 的持久化功能,确保队列数据不会丢失。
// 初始化一个队列,并放入元素 element1
final RQueue<String> queue=redisson.getQueue("myQueue");
queue.add("element1");
// 从队列中获取元素
String element=queue.poll();
Redisson Queue 使用 Redis 的 List 数据类型来存储队列的元素,队列的每个元素是 List 中的一个值。
在我们的项目场景中,我们使用它作为消息队列,用于多个系统间的异步通信。因为项目中已经引入了 Redis ,同时需要类似于消息队列的功能,那么就直接复用现有的中间件即可。
3.4 分布式 Set
跟 Java 里面的 Set 几乎是一样的,只不过是支持通过 Redis 去同步数据。 因为 Redis 有数据持久化,所以也不会重启应用也不会丢数据。
// 初始化一个集合
final RSet<String> set=redisson.getSet("mySet");
// 往集合中添加一个元素
set.add("item1");
// 移除集合中的一个元素
set.remove("item1");
存储是使用 Redis 的 Set 数据类型来存储集合元素,每个元素在集合中是唯一的。
实际项目中可以用来去数据进行去重等操作,或者是利用 Redis 的集合操作进行交集、并集等运算。
3.5 分布式 List
Redisson 中的 List 就是一种类似于 Java 的 List 接口的集合数据结构,可以适用于分布式环境。
// 初始化一个 List
final RList<String> list=redisson.getList("myList");
// 往列表中添加一个元素
list.add("element1");
// 获取列表中的首个元素
final String listElement=list.get(0);
本质上还是利用 Redis 来实现跨多个节点的数据共享。数据使用 Redis 的 List 数据类型来存储集合元素,元素的顺序也是按插入顺序存储。
在实际运用中,可以用来实现任务的顺序处理,适合生产者-消费者模型。
3.6 发布/订阅
发布/订阅并不是 Redisson 自己搞出来的新功能,而是基于 Redis 的发布/订阅机制 (Pub/Sub)。这点在 Jedis 以及 Lettuce 中也提供了类似的 API 接口。
不过区别点在于 Redisson 提供了高级封装,易于使用。
// 先实例化一个 topic name 为 myTopic , 然后再往 topic 发送一条消息 Hello, World!
final RTopic topic=redisson.getTopic("myTopic");
topic.publish("Hello, World!");
// 另外一个应用去订阅这个 topic 里面的消息
topic.addListener(String.class,new MessageListener<String>(){
@Override
public void onMessage(CharSequence channel,String msg){
System.out.println("Received: "+msg);
}
});
上面的例子就是 Redisson 发布订阅的基本用法,代码很简洁。不过本质上用的还是 Redis 的发布订阅,所以要注意使用上的限制。
3.7 分布式计数器
所谓的计数器就是类似于JDK 的 AtomicLong 等工具类。
// 初始化一个计数器
final RAtomicLong atomicLong=redisson.getAtomicLong("myAtomicLong");
// 自增1
atomicLong.incrementAndGet();
计数器是使用 Redis 的 String 数据类型存储数值,自增等操作也是使用 Redis 的INCRBY 命令、DECRBY 命令 等进行操作。
在实际项目中,比较常用的几点:
- 当做一个ID生成器,类似于数据库自增主键的作用。
- 用于限制操作次数,如 API 调用计数。
- 实现多个应用间的计数,如页面访问量、库存数量等等。
4. 使用 Redisson 比较常见的问题
主要的几个问题,在 Redisson 的文档里面其实都有提到过,在实际项目中也确实是比较高的频率出现。
4.1 导致 RedisTimeoutException 的原因
因为 Redisson 是基于 Netty 开发,所以超时这块大部分时候都是 Netty 连接相关的问题。
通常出现的原因有:
- 所有 Netty 线程都忙于处理,导致 Redis 响应解码和发送命令延迟。
- Netty 所有连接都在使用中,很繁忙无法处理其他操作。
- Redis服务器处理请求时间过长。
- Java 应用程序自身处理逻辑过多,没有响应之类的。
- 在 subscribeOnElements 方法中进行阻塞调用。
- 不稳定的网络导致 TCP 丢包。
- Redis服务商限制了并发连接数量上限。
解决方案:
- 尝试为 nettyThreads 设置不同的值(如32、64、128、256),以便 Redisson 可以获得一个空闲的 Netty 线程来解码响应或发送命令。
- 增加 retryInterval 或 timeout 的值,以便在命令超时前能优雅地失败。
- 增加连接池的设置,以提高获取空闲连接的概率。
说到底就是 Netty 是一个异步事件驱动的网络应用框架,使用多线程处理I/O操作,线程不够用了就容易出现这个错误。
4.2 Redisson 需不需要手动关闭
答案是可以手动去关闭,这样可以释放一些连接 Redis 的连接资源。 Redisson 本质上只是一个客户端库,本身是通过网络连接获取 Redis 中的数据。手动关闭 Redisson 可以提早释放网络连接,这一点上 跟 Jedis 、Lettuce 没有很大的区别。
通常 Redisson 跟着 Java 应用程序的启动和停止。关闭过程会断开所有的活动连接,并清理某些需要手动销毁的Redisson对象,最后停止事件循环。
4.3 MapCache/SetCache/SpringCache/JCache 到了过期时间数据没有被删除
Redis 本身不支持 Hash 、Set 等过期的,这些类型的数据过期都是 Redisson 提供的特性。
Redisson 采用了主动和被动两种方式确保元素在过期时被移除。
- 定期运行就是定期去扫描 Redis 中过期元素,发现有过期的元素就会去删除。
- 访问元素的时候也会检查元素是否过期,过期就会立马删除掉。
从上面两点可以看出, Redisson 的线程运行的时候或者说 Redisson 的 API 调用的时候,设置的元素过期才会生效。 如果 Redisson 线程不在运行的话,上面这些元素是永远过期了都不会删除的。
4.4 Redisson 线程安全问题
Redisson 实例及其提供的所有对象都是线程安全的。API只是操作句柄,保持线程安全。所以可以在多个线程间共享实例,全局公用一个 Redisson 实例即可。
4.5 数据编码问题
Redisson 中可以设置一个全局默认的数据编解码器,也可以在每一个 API 调用的时候指定单独的编解码器。
比如:
final RMap<String, String> map = redisson.getMap("myMap", new MyCodec());
4.6 跟其他应用 Spring Data Redis 协作的时候序列化问题
如果出现跨项目,一个项目使用 Spring Data Redis ,另外一个项目使用 Redisson 的话,两边也是可以进行操作同一份数据的。需要注意的是设置好两边 value 的反序列以及序列化策略,保持一致即可。
Spring Data Redis 使用 Jackson 作为序列化器:
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
final RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
Redisson 使用 JsonJacksonCodec 作为序列化器:
codec: !<org.redisson.codec.JsonJacksonCodec> {}
这种配置确保了 Redisson 和 Spring Data Redis 都使用 Jackson 进行 JSON 序列化和反序列化,这样就可以公用数据。
参考链接
- Consider Lettuce instead of Jedis as default Redis driver dependency https://github.com/spring-projects/spring-boot/issues/10480
- Consider replacing Jedis with Lettuce in spring-boot-starter-data-redis https://github.com/spring-projects/spring-boot/issues/9536
- Jedis issue : consider make a new release https://github.com/redis/jedis/issues/1576
- Redisson FAQ https://github.com/redisson/redisson/wiki/16.-FAQ