经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » Redis » 查看文章
基于Redis实现阻塞队列的方式
来源:jb51  时间:2021/12/20 15:28:27  对本文有异议

日常需求开发过程中,不免会遇到需要通过代码进行异步处理的情况,比如批量发送邮件,批量发送短信,数据导入,为了减少用户的等待,不希望一直菊花转啊转,因此需要进行异步处理,做法就是讲要处理的数据添加到队列当中,然后按照排队的先后顺序进行异步处理。

这个队列,可以是专业的消息队列,如 RocketMQ/RabbitMQ 等,一般项目中,如果只是为了进行异步,未免有点杀鸡用牛刀的意味。
也可以使用基于 JVM 内存实现队列,但是如果项目进行了重启,就会造成队列数据丢失。
大部分的项目都会用到 Redis 中间件作为缓存使用,此时使用 Redis 的 list 结构来实现队列则是非常合适的选择。

因此,本文主要讲解基于 Redis 的方式实现异步队列。

本文首发个人技术博客: https://nullpointer.pw/redis-block-queue.html

基于 Redis 的 list 实现队列的方式也有多种,先说第一种不推荐的方式,即使用LPUSH生产消息,然后 while(true) 中通过RPOP消费消息,这种方式的确可以实现,但是不断代码不断的轮询,势必会消耗一些系统的资源。

第二种方式也是不推荐的方式,也是通过 LPUSH生产消息,然后通过 BRPOP 进行阻塞地等待并消费消息,这种方式较第一种方式减少了无用的轮询,降低系统资源的消耗,但是可能会存在队列消息丢失的情况,如果取出了消息然后处理失败,这个被取出的消息就将丢失。

第二种方式就是下文要介绍的方式,首先也是通过 LPUSH 生产消息,然后通过 BRPOPLPUSH阻塞地等待 list 新消息到来,有了新消息才开始消费,同时将消息备份到另外一个 list 当中,这种方式具备了第二种方式的优点,即减少了无用的轮询,同时也对消息进行了备份不会丢失数据,如果处理成功,可以通过 LREM 对备份的 list 中当前的这条消息进行删除处理。这种方式实现方式可以参考 模式: 安全的队列 .

Redis 基础

  1. # 将一个或多个值 value 插入到列表 key 的表头
  2. LPUSH key value [value …]
  3.  
  4. # 阻塞式等待,将列表 source 中的最后一个元素 (尾元素) 弹出,并返回给客户端。将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。超时参数 timeout 接受一个以秒为单位的数字作为值。超时参数设为 0 表示阻塞时间可以无限期延长 (block indefinitely) 。
  5. BRPOPLPUSH source destination timeout
  6.  
  7. # 根据参数 count 的值,移除列表中与参数 value 相等的元素。
  8. LREM key count value

代码实现队列消息生产者

笔者使用的是 Spring 相关 API 实现对 Redis 指令的调用。首先实现消息的生产代码,封装到一个工具类方法当中。这里很简单,就是调用了 lpush 方法,将序列化的 key 和 value 添加到列表当中去。

  1. @Resource
  2. private RedisConnectionFactory connectionFactory;
  3.  
  4. public void lPush(@Nonnull String key, @Nonnull String value) {
  5. RedisConnection connection = RedisConnectionUtils.getConnection(connectionFactory);
  6. try {
  7. byte[] byteKey = RedisSerializer.string().serialize(getKey(key));
  8. byte[] byteValue = RedisSerializer.string().serialize(value);
  9. assert byteKey != null;
  10. connection.lPush(byteKey, byteValue);
  11. } finally {
  12. RedisConnectionUtils.releaseConnection(connection, connectionFactory);
  13. }
  14. }

代码实现队列消息消费者

因为实现队列消费消息的代码比较多,不可能每个需要阻塞消费的地方,对需要写这一坨代码,因此使用 Java8 的函数式接口实现方法的传递,同时阻塞式获取消息代码使用新线程去执行。

有人看到以下代码要吐槽了,不是说不用 while(true) 吗,怎么你这里面还是有,这里稍微解释一下,因为 SpringBoot 一般会指定 timeout 的全局超时时间,即使 BRPOPLPUSH 设置了 0,即无限期,当超出了 timeout 设置的值时,就会抛出 QueryTimeoutException 异常导致线程退出,因此添加了 try/catch 对异常进行捕获并忽略,同时使用 while(true) 保证线程可以继续执行。
代码中记录了当前消息处理结果,如果处理结果为成功,需要对备份队列的当前消息进行删除。

  1. public void bRPopLPush(@Nonnull String key, Consumer<String> consumer) {
  2. CompletableFuture.runAsync(() -> {
  3. RedisConnection connection = RedisConnectionUtils.getConnection(connectionFactory);
  4. try {
  5. byte[] srcKey = RedisSerializer.string().serialize(getKey(key));
  6. byte[] dstKey = RedisSerializer.string().serialize(getBackupKey(key));
  7. assert srcKey != null;
  8. assert dstKey != null;
  9. while (true) {
  10. byte[] byteValue = new byte[0];
  11. boolean success = false;
  12. try {
  13. byteValue = connection.bRPopLPush(0, srcKey, dstKey);
  14. if (byteValue != null && byteValue.length != 0) {
  15. consumer.accept(new String(byteValue));
  16. success = true;
  17. }
  18. } catch (Exception ignored) {
  19. // 防止获取 key 达到超时时间抛出 QueryTimeoutException 异常退出
  20. } finally {
  21. if (success) {
  22. // 处理成功才删除备份队列的 key
  23. connection.lRem(dstKey, 1, byteValue);
  24. }
  25. }
  26. }
  27. } finally {
  28. RedisConnectionUtils.releaseConnection(connection, connectionFactory);
  29. }
  30. });
  31. }

测试代码

  1. @Test
  2. public void testLPush() throws InterruptedException {
  3. String queueA = "queueA";
  4. int i = 0;
  5. while (true) {
  6. String msg = "Hello-" + i++;
  7. redisBlockQueue.lPush(queueA, msg);
  8. System.out.println("lPush: " + msg);
  9. Thread.sleep(3000);
  10. }
  11. }
  12.  
  13. @Test
  14. public void testBRPopLPush() {
  15. String queueA = "queueA";
  16. redisBlockQueue.bRPopLPush(queueA, (val) -> {
  17. // 在这里处理具体的业务逻辑
  18. System.out.println("val: " + val);
  19. });
  20.  
  21. // 防止 Junit 进程退出
  22. LockSupport.park();
  23. }

项目使用方式

为了方便使用,我将其抽取为了一个工具类,使用时通过 Spring 注入使用即可,
队列消费可以使用如下方式在项目启动的时候就进行阻塞监听队列,等待消费

  1. @Resource
  2. private RedisBlockQueue redisBlockQueue;
  3.  
  4. @PostConstruct
  5. public void init() {
  6. redisBlockQueue.bRPopLPush(xx, (value) -> {
  7. //...
  8. });
  9. }

本文完整代码下载github 地址

到此这篇关于基于Redis实现阻塞队列的文章就介绍到这了,更多相关Redis阻塞队列内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号