Redis简单延时队列

Redis实现简单延队列, 利用zset有序的数据结构, score设置为延时的时间戳. 

实现思路:

1、使用命令 [zrangebyscore keyName socreMin socreMax] 会返回已score排序由小到大的一个list

2、list非空则使用[zrem keyName value]  删除第一个元素, 删除成功即代表消费成功, 可以解决多线程并发消费的问题.

 

使用jedis实现代码:

  1 package com.nancy.utils;
  2 
  3 import com.alibaba.fastjson.JSON;
  4 import com.alibaba.fastjson.TypeReference;
  5 import redis.clients.jedis.Jedis;
  6 
  7 import java.lang.reflect.Type;
  8 import java.util.*;
  9 
 10 /**
 11  * redis实现延时队列,但是对于要求行极高的环境不建议使用,主要原因:
 12  * 1、没有可靠的消息持久机制,消息容易丢失
 13  * 2、ack应答机制确实,没有传统MQ机制的可靠性
 14  *
 15  * @author  zhou.guangfeng on 2019/3/9 下午4:31
 16  */
 17 public class RedisDelayQueue<T> {
 18 
 19     static class TaskItem<T>{
 20         public String id ;
 21         public T msg ;
 22     }
 23 
 24     private Jedis jedis ;
 25     private String queueKey ;
 26     private Type taskType = new TypeReference<TaskItem<T>>(){}.getType();
 27 
 28     RedisDelayQueue(Jedis jedis, String queueKey){
 29         this.jedis = jedis ;
 30         this.queueKey = queueKey ;
 31     }
 32 
 33     public void delay(T msg){
 34         TaskItem<T> item = new TaskItem<>() ;
 35 
 36         item.id = UUID.randomUUID().toString() ;
 37         item.msg = msg ;
 38 
 39         String content = JSON.toJSONString(item) ;
 40         try {
 41             Thread.sleep(10L);
 42         } catch (InterruptedException e) {
 43             e.printStackTrace();
 44         }
 45         jedis.zadd(queueKey, System.currentTimeMillis() + 5000, content) ;
 46     }
 47 
 48     public void loop(){
 49         while (!Thread.interrupted()){
 50             Boolean flag = consumer() ;
 51             try {
 52                 if(!flag) {
 53                     Thread.sleep(500L);
 54                 }
 55             }catch (InterruptedException ex){
 56                 break;
 57             }
 58 
 59         }
 60     }
 61 
 62     /**
 63      *
 64      * 队列消费,利用zrem操作,删除成功即也消费。 并发环境可能出现zrem删除失败情况,从而导致无效的请求。
 65      * @param
 66      * @return
 67      */
 68     private Boolean consumer(){
 69          // 按照分数即时间, 有序集成员按 score 值递增(从小到大)次序排列。
 70          Set<String> values = jedis.zrangeByScore(queueKey, 0, System.currentTimeMillis()) ;
 71          if (values == null || values.isEmpty()){
 72             return false ;
 73          }
 74 
 75          String content = values.iterator().next() ;
 76          if(jedis.zrem(queueKey, content) <= 0){
 77              return false ;
 78          }
 79 
 80         TaskItem<T> item = JSON.parseObject(content, taskType) ;
 81         handleMsg(item.msg) ;
 82         return true ;
 83     }
 84 
 85     public void handleMsg(T msg){
 86         System.out.println(msg);
 87     }
 88 
 89     public static void main(String[] args) {
 90         RedisDelayQueue<String> queue = new RedisDelayQueue<>(AbstractDistributedLock.getJedisPool().getResource(), "delay-queue-demo") ;
 91 
 92         System.out.println("delay queue start, time = " + new Date());
 93         Thread producer = new Thread(){
 94             @Override
 95             public void run() {
 96                 for (int i = 0; i < 10; i++) {
 97                     queue.delay("codehole:" + i);
 98                 }
 99             }
100         };
101 
102         Thread consumer = new Thread(){
103             @Override
104             public void run() {
105                 queue.loop();
106             }
107         };
108 
109         producer.start();
110         consumer.start();
111 
112         try {
113             producer.join();
114             Thread.sleep(6000L);
115 
116             consumer.interrupt();
117             consumer.join();
118         }catch (InterruptedException ex){
119 
120         }finally {
121             System.out.println("delay queue start, end = " + new Date());
122         }
123 
124     }
125 
126 }

 

 

 

 1 public abstract class AbstractDistributedLock implements RedisLock {
 2 
 3     private static JedisPool jedisPool;
 4 
 5     protected static final String LOCK_SUCCESS = "OK";
 6     protected static final Long RELEASE_SUCCESS = 1L;
 7     protected static final String SET_IF_NOT_EXIST = "NX";
 8     protected static final String SET_WITH_EXPIRE_TIME = "EX";
 9     protected static final long DEFAULT_EXPIRE_TIME = 1000 * 10 ;
10     protected static final long DEFAULT_DELAY_TIME = 2 ;
11 
12     static {
13         JedisPoolConfig config = new JedisPoolConfig();
14         // 设置最大连接数
15         config.setMaxTotal(500);
16         // 设置最大空闲数
17         config.setMaxIdle(50);
18         // 设置最大等待时间
19         config.setMaxWaitMillis(1000 * 100);
20         // 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的
21         config.setTestOnBorrow(true);
22         jedisPool = new JedisPool(config, "127.0.0.1", 6379, 3000);
23     }
24 
25     public static JedisPool getJedisPool() {
26         return jedisPool;
27     }
28 
29     @Override
30     public String getLockKey(String lockKey){
31         return  "lock:" + lockKey;
32     }
33 
34 }
AbstractDistributedLock

 

 

运行结果:

delay queue start, time = Mon Mar 11 10:21:25 CST 2019
codehole:0
codehole:1
codehole:2
codehole:3
codehole:4
codehole:5
codehole:6
codehole:7
codehole:8
codehole:9
delay queue start, end = Mon Mar 11 10:21:31 CST 2019

 

 

以上的代码 jedis.zrangeByScore 和  jedis.zrem 为非原子操作.  如果jedis.zrem一旦失败, 会进入休眠, 造成资源浪费. 因此改造为使用lua脚本执行jedis.zrangeByScore 和  jedis.zrem 保证原子性.

 1    /**
 2      * 队列消费 使用lua脚本, 保证zrangebyscore 和 zrem操作原子性。
 3      *
 4      * @param
 5      * @return
 6      */
 7     private Boolean consumerWithLua(){
 8         String script = " local resultDelayMsg = {}; " +
 9                         " local arr = redis.call('zrangebyscore', KEYS[1], '0', ARGV[1]) ; " +
10                         " if next(arr) == nil then return resultDelayMsg  end ;" +
11                         " if redis.call('zrem', KEYS[1], arr[1]) > 0 then table.insert(resultDelayMsg, arr[1]) return resultDelayMsg end ; " +
12                         " return resultDelayMsg ; ";
13         Object result = jedis.eval(script, Collections.singletonList(queueKey), Collections.singletonList("" + System.currentTimeMillis()));
14         List<String> msg = null ;
15         if (result == null || (msg = (List<String>) result).isEmpty()) {
16             return false ;
17         }
18 
19         TaskItem<T> item = JSON.parseObject(msg.get(0), taskType) ;
20         handleMsg(item.msg) ;
21         return true ;
22     }

 

 redis实现延时队列,但是对于要求行极高的环境不建议使用,主要原因:

1、没有可靠的消息持久机制,消息容易丢失. 需要自己实现

2、ack应答机制缺失,没有传统MQ机制的可靠性

 

因此, 如果对数据一致性有严格的要求, 还是建议使用传统MQ.

 

posted on 2019-03-12 10:15  小猩  阅读(584)  评论(0编辑  收藏  举报

导航