基于redis实现分布式延时队列

为什么要使用延时队列

1,降低数据库的tps

有些数据需要频繁更新,使用延时队列降低数据库tps。例如车辆位置数据1秒更新一次,一小时更新3600次,如果使用延迟5分钟更新一次一小时只更新12次。大大降低了数据库压力

2,分布式任务

使用分布式任务降低单节点压力,例如1W个用户在同一时刻执行任务,如果是单节点有可能线程过多任务很久才能执行完

为什么要用redis

  1. redis使用很普遍
  2. zset数据结构既有排序又有set结构
  3. 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/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值