基于Redis实现分布式延时队列
为什么要使用延时队列
1,降低数据库的tps
有些数据需要频繁更新,使用延时队列降低数据库tps。例如车辆位置数据1秒更新一次,一小时更新3600次,如果使用延迟5分钟更新一次一小时只更新12次。大大降低了数据库压力
2,分布式任务
使用分布式任务降低单节点压力,例如1W个用户在同一时刻执行任务,如果是单节点有可能线程过多任务很久才能执行完
为什么要用redis
- redis使用很普遍
- zset数据结构既有排序又有set结构
- redis的一些操作具有原子性可以实现分布式锁
分布式延时队列原理
要实现分布式锁
要实现分布式队列必须要实现分布式锁。redis很多操作具有原子性例如 SETNX ,GETSET 等。例如10个线程同时执行SETNX,其中只能有一个线程执行成功。
带排序队列的Set结构
Redis 带排序集合既有排序功能又有Set结果。带排序的队列才能实现延迟,例如java的DelayQueue,但他没有Set结构。因为我们要用Set结构去重,如果没有Set结构定位某个数据时间复杂度相当于O(n)。
上代码
Redis锁以及相关操作
@Component
public class RedisManager {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 轮询竞争锁
* @param lockKey 锁key
* @param intervalMs 轮询间隔,太长一直轮询不到,太短redis请求频繁资源浪费
* @param lockTimeoutTime 锁超时时间
* @return
*/
public String lockUntil(String lockKey,long intervalMs,long lockTimeoutTime) {
String lockValue = null;
while (!lock(lockKey,lockValue = String.valueOf(System.currentTimeMillis()+lockTimeoutTime))){
try {
Thread.sleep(intervalMs);
} catch (InterruptedException e) {
}
}
return lockValue;
}
/**
* 竞争加锁 原理是setIfAbsent,getAndSet具有原子性,只有一个线程成功执行
* @param key
* @param value
* @return
*/
public boolean lock(String key, String value) {
//System.out.println(Thread.currentThread().getName()+" try lock " + value);
if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
//System.out.println(Thread.currentThread().getName()+" lock " + value);
return true;
}
//判断未解锁情况,加超时判断
String curVal = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(curVal) && Long.parseLong(curVal) < System.currentTimeMillis()) {
//获得之前的key值,同时设置当前的传入的value。这个地方可能几个线程同时过来,但是redis本身天然是单线程的,所以getAndSet方法还是会安全执行,
//首先执行的线程,此时curVal当然和oldVal值相等,因为就是同一个值,之后该线程set了自己的value,后面的线程就取不到锁了
String oldVal = redisTemplate.opsForValue().getAndSet(key, value);
if(!StringUtils.isEmpty(oldVal) && oldVal.equals(curVal)) {
//System.out.println(Thread.currentThread().getName()+" timesout lock " + value);
return true;
}
}
return false;
}
/**
* 释放锁
* @param key
* @param value
*/
public void unlock(String key, String value) {
//System.out.println(Thread.currentThread().getName()+" unlock " + value);
try {
String curVal = redisTemplate.opsForValue().get(key);
//System.out.println(Thread.currentThread().getName()+" lockvalue " + curVal);
if (!StringUtils.isEmpty(curVal) && curVal.equals(value)) {
redisTemplate.opsForValue().getOperations().delete(key);
//System.out.println(Thread.currentThread().getName()+" unlocked " + value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* redis 字符串set
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key,value);
}
/**
* redis 字符串get
* @param key
* @return
*/
public String get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* zset 添加单个
* @param queueName
* @param key
* @param score
*/
public void addQueue(String queueName, String key, Double score){
redisTemplate.opsForZSet().add(queueName,key,score);
}
/**
* zset 添加批量
* @param queueName
* @param tuples
*/
public void addQueueBatch(String queueName, Set<ZSetOperations.TypedTuple<String>> tuples){
redisTemplate.opsForZSet().add(queueName,tuples);
}
/**
* zset 通过索引批量获取数据
* @param queueName
* @param start
* @param end
* @return
*/
public Set<String> getQueueBatch(String queueName, long start, long end){
return redisTemplate.opsForZSet().range(queueName,start,end);
}
/**
* 统计到期队列数量
* @param queueName
* @return
*/
public Long countExpireQueue(String queueName,long expiresMs){
return redisTemplate.opsForZSet().count(queueName,0, expiresMs);
}
/**
* 统计所有数据
* @param queueName
* @return
*/
public Long countAllQueue(String queueName){
return redisTemplate.opsForZSet().count(queueName,0, Long.MAX_VALUE);
}
/**
* 通过索引移除元素
* @param queueName
* @param start
* @param end
* @return
*/
public Long removeQueueBatch(String queueName, long start, long end){
return redisTemplate.opsForZSet().removeRange(queueName,start,end);
}
/**
* 返回元素位置
* @param queueName
* @param key
* @return
*/
public Long getIndexQueue(String queueName, Object key){
return redisTemplate.opsForZSet().rank(queueName,key);
}
/**
* 移除元素
* @param queueName
* @param keys
* @return
*/
public Long remove(String queueName, Object... keys){
return redisTemplate.opsForZSet().remove(queueName,keys);
}
}
队列相关操作
public class RedisDelayDistinctQueue {
/**
* 延迟时间,默认30分钟(现在入队30分钟后才可以取出来)
*/
private long expiresMs = 1000L * 60 * 30;
/**
* 出队延迟偏移,默认0,如果是1000表示再延迟1秒出队,如果是-1000就是提前1秒出队
*/
private long expiresOffSetMs = 0;
/**
* 批量入队数量
*/
private int updateBatchSize = 20;
/**
* 批量出队数量
*/
private long getBatchSize = 200;
/**
* 队列名
*/
private String queueName;
/**
* 队列锁名默认队列名:lock
*/
private String queueLockKey;
/**
* 出队竞争锁轮训等待时间
*/
private long intervalMs = 20;
/**
* 锁过期时间
*/
private long queueLockTimeoutMs = 1000*10;
/**
* 空队轮询时间
*/
private long emptyQueueIntervalMs = 1000;
/**
* pool出队最小轮询时间
*/
private long minIntervalMs = 50;
/**
* redis基本操作
*/
private RedisManager redisManager;
/**
* 声明队列
* @param queueName 队列名
* @param redisManager
*/
public RedisDelayDistinctQueue(String queueName, RedisManager redisManager) {
if(queueName ==null||redisManager==null){
throw new NullPointerException("queueName or redisManager nust not be null");
}
this.queueName = queueName;
this.redisManager = redisManager;
this.queueLockKey = queueName+":lock";
}
/**
* 构造函数
* @param queueName 队列名
* @param expiresMs 延迟时间
* @param redisManager
*/
public RedisDelayDistinctQueue(String queueName, long expiresMs, RedisManager redisManager) {
this(queueName,redisManager);
this.expiresMs = expiresMs;
}
/**
* 按默认延迟入队
* @param key
* @return
*/
public int add(String key){
return add(key,System.currentTimeMillis()+expiresMs);
}
/**
* 按指定延迟入队
* @param key
* @param expires
* @return
*/
public int add(String key,long expires){
int updateCnt = 0;
Long index = redisManager.getIndexQueue(queueName,key);
if(index == null){
redisManager.addQueue(queueName,key,expires+0.0);
}
return updateCnt;
}
/**
* 按默认延迟批量入队
* @param list
* @return
*/
public int addBatch(List<String> list){
int updateCnt = 0;
Set<ZSetOperations.TypedTuple<String>> set = new HashSet<>(10);
for (int i = 0;i<list.size();i++){
double score = System.currentTimeMillis()+ expiresMs;
Long index = redisManager.getIndexQueue(queueName,list.get(i));
//判断队列是否已经存在,如果存在会更新过期时间,导致没有定时消费数据
if(index == null){
DefaultTypedTuple tuple = new DefaultTypedTuple(list.get(i),score);
set.add(tuple);
if((i+1)%updateBatchSize == 0){
redisManager.addQueueBatch(queueName,set);
updateCnt += set.size();
set.clear();
}
}
}
if(set.size()>0){
redisManager.addQueueBatch(queueName,set);
updateCnt += set.size();
}
return updateCnt;
}
/**
* 阻塞式批量获取队列数据,阻塞轮询redis时间默认1秒
* @return
*/
public Set<String> takeBatch(){
return takeBatch(0);
}
/**
* 带延迟偏移阻塞式批量获取队列数据,阻塞轮询redis时间默认1秒
* @param expiresOffSetMs 延迟偏移正数推迟消费,负数提前消费
* @return
*/
public Set<String> takeBatch(long expiresOffSetMs){
boolean haveData = false;
Set<String> set = null;
while(!haveData){
set = pollBatch(true,expiresOffSetMs);
if(set.size()==0){
try {
Thread.sleep(emptyQueueIntervalMs);
} catch (InterruptedException e) {
}
}else {
haveData = true;
}
}
return set;
}
/**
* 批量获取队列数据,超时返回,阻塞轮询redis时间默认最小50毫秒,最大1秒,可以通过设置expiresOffSetMs,消费偏移
* @param timeoutMs 超时返回时间
* @return
*/
public Set<String> pollBatch(long timeoutMs){
boolean haveData = false;
Set<String> set = null;
long intervalMs = timeoutMs/10;
if(intervalMs <minIntervalMs){
intervalMs = minIntervalMs;
}else if(intervalMs>emptyQueueIntervalMs){
intervalMs = emptyQueueIntervalMs;
}
long begin = System.currentTimeMillis();
while(!haveData){
set = pollBatch();
if(System.currentTimeMillis()-begin>timeoutMs){
return set;
}
if(set.size()==0){
try {
Thread.sleep(intervalMs);
} catch (InterruptedException e) {
}
}else {
haveData = true;
}
}
return set;
}
/**
* 批量获取队列数据
* @return
*/
public Set<String> pollBatch(){
return pollBatch(true,expiresOffSetMs);
}
/**
* 获取数据不删除
* @return
*/
public Set<String> peekBatch(){
return pollBatch(false);
}
/**
* 获取数据不删除
* @param expiresOffSetMs 消费偏移
* @return
*/
public Set<String> peekBatch(long expiresOffSetMs){
return pollBatch(false,expiresOffSetMs);
}
private Set<String> pollBatch(boolean isDeleted){
return pollBatch(isDeleted,0);
}
/**
* 批量取数据
* @param isDeleted 去玩是否删除
* @param expiresOffSetMs 消费偏移
* @return
*/
private Set<String> pollBatch(boolean isDeleted, long expiresOffSetMs){
String lockValue = redisManager.lockUntil(queueLockKey,intervalMs,queueLockTimeoutMs);
Long cnt = redisManager.countExpireQueue(queueName,System.currentTimeMillis()-expiresOffSetMs);
long step = cnt;
if(cnt!=null && cnt>getBatchSize){
step = getBatchSize;
}
Set<String> set = null;
if(step >0){
set = redisManager.getQueueBatch(queueName,0,step);
if(isDeleted){
redisManager.removeQueueBatch(queueName,0,step);
}
}else{
set = new HashSet<>(0);
}
redisManager.unlock(queueLockKey, lockValue);
return set;
}
/**
* 判断是否包含
* @param key
* @return
*/
public boolean contains(String key){
return redisManager.getIndexQueue(queueName,key)==null?false:true;
}
/**
* 返回key值所在队列位置
* @param key
* @return 没有返回-1
*/
public long indexOf(String key){
Long index = redisManager.getIndexQueue(queueName,key);
return index==null?-1:index;
}
/**
* 整个队列大小
* @return 没有返回 0
*/
public long size(){
return redisManager.countAllQueue(queueName);
}
/**
* 待消费队列大小
* @return 没有返回0
*/
public long sizeExpired(){
return redisManager.countExpireQueue(queueName,expiresOffSetMs);
}
/**
* 移除对个key
* @param keys
* @return 没有返回0
*/
public long remove(String... keys){
return redisManager.remove(queueName,keys);
}
}
参考
[1]: redis api http://doc.redisfans.com/
[2]: redis api http://redisdoc.com/