首页 > 数据库 >Redis定长队列设计与实现

Redis定长队列设计与实现

时间:2023-05-04 20:59:31浏览次数:39  
标签:redis return 队列 Redis list 定长 key public

业务背景:
只展示最近10条礼物打赏动态,用户名+礼物名称
不管在app端还是在web端,或多或少都有这样的需求,所谓技术方案的选型都是受限于实际的业务场景的,都是以解决实际业务为目的,由于刚开始这样的需求还是比较少的,所以采用了简单的方式实现了功能,但是随着业务扩大,重复的也会很多,再写一套代码就显得很臃肿了 ,所以将这类业务进行抽象封装。

如上图实时展示了最近打赏的动态,通过这些来刺激消费和曝光用户,同时也是丰富页面,像这类动态不需要全部的数据,也不需要太多的历史数据,而是最近的10条数据,虽然数据量不是很大,但是要求实时性,所以需要有一个队列来存储这些数据,并且是有序的,由于这里项目用的RocketMQ与Redis,对比之后发现redis的效果更好也容易实现一些,利用redis的list结构来实现一个简单的动态消息定长队列,主要思路如下:
生产者:在队列里添加数据(rpush),每次往队列里添加数据的时候判断队列的总长度(llen),如果大于要求的长度10,则剔除第一个元素(lpop)
消费者:这里严格意义上不是消费者,只是读取数据可以全部读取,但是建议lrange(0, 10)来读取数据
以往粗略的实现方式为先获取长度(llen),再判断是否需要lpop,最后rpush来实现定长队列,保证队列长度的可控(因为多了也没用)
但是llen+lpop+rpush这个过程是非原子性的,所以这里采用lua脚本来保证原子性并且不用三次调用redis
list基本命令回顾

LLEN key
返回list的长度,时间复杂度O(1) 如果key不存在,则返回0,如果key非list类型,返回error

LPOP key [count]
删除并返回元素,时间复杂度为O(N),N为移除元素的个数,默认会返回第一个

RPUSH key element [element ...]
从list右侧插入元素,时间复杂度为O(N),N为插入元素个数,返回值为插入之后list的长度

整合上述三个命令,我们可以实现固定长度的队列,通过判断队列长度是否达到定长结合新增队列元素和移除队列元素来完成,整体命令的复杂度都是O(n)的常量时间
定义Lua脚本

local key = KEYS[1]
local num = tonumber(ARGV[1])
local val = ARGV[2]
if (redis.call('llen', key) >= num) then redis.call('lpop', key) end
redis.call('rpush', key, val)

这里整个key不设置过期时间,整个脚本也不返回值,默认成功

java代码封装

public interface IQueue<E> {
    /**
     * 入队操作
     * @param e 入队数据
     * @return
     */
    boolean offer(E e);

    /**
     * 出队
     * @return 一条队列数据
     */
    E remove();

    /**
     * 队列大小
     * @return
     */
    long size();
}

这里最好不要实现jdk的Queue,由于继承了Collection,所以会有很多无用的方法,所以自定义或者改造项目已有的队列接口

/**
 * redis定长队列
 *
 * @author liufuqiang
 * @since 2023/5/4
 */
public class RedisLimitQueue implements IQueue<String> {

    /**
     * redis操作模版
     */
    private RedisTemplate<String, String> redisQueueTemplate;

    /**
     * 存储key值
     */
    private String key;

    /**
     * 队列总长度
     */
    private Integer length;

    public RedisLimitQueue(RedisTemplate<String, String> redisQueueTemplate, String key, Integer length) {
        this.redisQueueTemplate = redisQueueTemplate;
        this.key = key;
        this.length = length;
    }

    private static final String LIMIT_OFFER_LUA =
            "local key = KEYS[1]" +
            "local num = tonumber(ARGV[1])" +
            "local val = ARGV[2]" +
            "if (redis.call('llen', key) >= num) then redis.call('lpop', key) end " +
            "redis.call('rpush', key, val)";

    public List<String> list() {
        return redisQueueTemplate.opsForList().range(key, 0, length);
    }

    @Override
    public boolean offer(String s) {
        DefaultRedisScript script = new DefaultRedisScript(LIMIT_OFFER_LUA);
        redisQueueTemplate.execute(script, Collections.singletonList(key), String.valueOf(length), s);
        return true;
    }

    @Override
    public String remove() {
        return null;
    }

    @Override
    public long size() {
        return redisQueueTemplate.opsForList().size(key);
    }
}

由于上述代码在公用项目里面供多个微服使用,所以在初始化的时候,按需引入,一般结合nacos动态配置队列的长度

@Bean(name = "cardLimitQueue")
	public RedisLimitQueue cardLimitQueue(@Qualifier("cacheQueueRedisTemplate") StringRedisTemplate cacheQueueRedisTemplate) {
		return new RedisLimitQueue(cacheQueueRedisTemplate, key, length);
	}

由此就完成了简单的定长队列,后续有不同的业务要求,只需新增一个key,就可以完成定长要求。
上述Lua脚本首先执行了llen,熟悉rpush命令的可能一眼能看出来,这个步骤略微有点多余,因为rpush的返回值就是插入元素之后list的长度,所以可以用下面写法代替,减少了llen的过程

这里加入要10条数据,上述脚本在并发的情况下可能返回11条数据,如果对数据总长度严格要求的话,上述写法不可取

*虽然这里简单实现了,但是还有些细节,就以文章开头的图片打赏礼物为例。假如第一条数据为用户张三打赏了A礼物,第二条数据第三条数据还是张三这个人,并且打赏的礼物还是A,那么这个时候出现了刷屏的现象,对别的用户不管是感官上还是页面呈现,大概率会出现不适的情况,争对这种情况,需要多的就是去重,由于使用list未具备去重的功能,这里如果不需要出现重复的数据,可以提供两种思路:
1、list实现的定长队列改为zset的zcard+zadd+zpopmin来实现,但是这里zset肯定没用list效果好
2、根据业务主键进行加锁,比如这里需要展示的数据更真实一些,采用用户ID+礼物ID作为主键的key来进行加锁,根据业务量来设置过期时间

总结:
本文主要探索在特定业务场景下通过Redis的原生命令实现类MQ的功能,创新式的通过Lua脚本组合Redis的List的基础命令,整体解决方案在线上环境落地并平稳运行,为特定场景提供了一种通用的解决方案。

标签:redis,return,队列,Redis,list,定长,key,public
From: https://www.cnblogs.com/LiuFqiang/p/17372463.html

相关文章

  • 安装redis
    服务器下执行cd/usr/local,进入到local文件中。执行mkdirredis创建文件夹。 下载redis-6.0.6.tar.gz文件,当前目录执行tar-xvfredis-6.0.6.tar.gz将文件解压。在b服务器安装gcc,centos:执行yuminstallgcc-c++,在执行yum-yinstallcentos-release-scl,yum-yinstall......
  • 【Redis】一次报错小记
    背景当时是这样子的,业务系统开发,当时主责开发一个模块,突然某一天,它就启动不了了,报了一个错:ERRThisinstancehasclustersupportdisabled(Redis的报错提示)嗯,就是这个东西。很迷,一直都是好的,咋突然就不行了呢?明明就没有用到redis,怎么会报这个错呢,虽然依赖里面引入了,但......
  • Celery - 分布式任务队列
    Celery-分布式任务队列目录Celery-分布式任务队列1celery简介1.1什么是celery1.2celery架构(1)消息中间件messagebroker(2)任务执行单元worker(3)任务结果存储taskresultstore(4)使用场景2Celery安装与使用2.1安装2.2快速使用①第1步:创建celeryapp与创建任务②第2步......
  • django-channel 配置 channel layer 添加redis的账号和密码
    最近公司要使用django-channel搭建socket, 文档:https://channels.readthedocs.io/en/stable/introduction.html文档里面并没有写如果redis有账号和密码的话,怎么配置。配置方法:https://github.com/django/channels/issues/164#issuecomment-220513297如下:CHANNEL_LAYERS......
  • 我设计了个【方案】:比redis好10倍的kv库【一统kv】
    我设计的redis9.0方案:redis自带中间件基于ssd磁盘,此我设计了比redis更好的缓存方案。此方案:没有缓存击穿问题。没有缓存雪崩问题。没有缓存污染问题。没有热key问题。不需要snap和aof。支持任何sql库,sql库不需要带有任何分布式功能。 基于ssd磁盘,此我设计了比redis更好的缓存方......
  • redis集成
    1、linux下源码安装redis官网http://redis.io下载redis解压tar-zvsfredis-7.0.11.tar.gz移动到usr/local路径下mvredis-7.0.11/usr/local/redis进入redis文件夹cd/usr/local/redis使用两个cpu编译文件,速度快点。因为服务器是2核,根据自己配置即可make-j2将编......
  • Linux安装部署Redis(超级详细)
    原文链接:https://www.cnblogs.com/AllWjw/p/15771097.html网上搜索了一筐如何在Linux下安装部署Redis的文章,各种文章混搭在一起勉强安装成功了。自己也记录下,方便后续安装时候有个借鉴之处。Redis版本5.0.4服务器版本LinuxCentOS7.664位下载Redis进入官网找到下载地......
  • 在Alibaba Cloud Linux操作系统上安装Redis教程
    AlibabaCloudLinux2内置Redis6.0.5和Redis3.2.12的yum源,执行sudoyuminstall命令即可部署Redis6.0.5和Redis3.2.12。本文阿里云百科以阿里云持久内存服务器ECS为例,使用AlibabaCloudLinux2.1903LTS64位操作系统,安装Redis6.0.5或Redis3.2.12详细流程如下:AlibabaCloud......
  • 消息队列
    sys/msg.h#include<sys/msg.h>intmain(void){//创建消息队列//通过key创建或获取消息队列返回消息队列ID失败返回-1/**msgget创建或获取消息队列*key:ftok函数返回的key*msgflg标志位置*0-获取不......
  • 【SpringBoot系列】七、SpringBoot 中使用Redis缓存
        在项目中对数据的访问往往都是直接访问数据库的方式,但如果对数据的访问量很大或者访问很频繁的话,将会对数据库来很大的压力,甚至造成数据库崩溃。为了解决这类问题redis数据库脱颖而出,redis数据库出现时是以非关系数据库的光环展示在广大程序猿的面前的,后来redis的迭代版......