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 }
运行结果:
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.