首页 > 技术文章 > Redis脚本设计高并发

jinliang374003909 2022-01-26 15:35 原文

需求场景:高并发接口场景

一:设计思路

​ 利用redis设计多级缓存(假设有2级),当一级缓存用完后,到二级缓存中查询同时开启异步线程填充一级缓存。如此不断循环可保障缓存中一直有数据。

1.多级缓存设计:共计创建3个list集合(2个为缓存集合,1个为维护缓存集合的状态)和1个hash集合(用于存放缓存中被消费的数据信息-key为被消费的id,value为其他信息)

2.异步填充缓存,同时通知(MQ)更新hash集合中数据到数据库。

步骤演示:

步骤一:当请求过来时,先判断loopQueue的长度

​ 1.若大于0—》从左获取第一个数据(list的key),找到对应的list(q1),并判断改list的长度

​ 1.1若大于0—》从list中取出该值并返回

​ 1.2否则 —》从左取出loopQueue得第一个数据,并回到步骤一
2.否则—》从数据库中获取

二:实现:

步骤一:预备数据——>创建缓存队列(lua脚本+redisTemplate)
1.脚本定义如下
local len = redis.call('LLEN', KEYS[1])
if len ~= 0 then  
    return false //list缓存队列不存在
end
local len = redis.call('HLEN', KEYS[2])
if len ~= 0 then
    return false //hash集合也不存在
    else
    redis.call('LPUSH', KEYS[1], ARGV[1],ARGV[2]) //创建缓存队列
    return true
end
2.redisTemplate调用脚本——createLoopQueue
DefaultRedisScript<Boolean> redisScript2 = new DefaultRedisScript<Boolean>();
List<String> keys = new ArrayList<>();
List<String> argv = new ArrayList<>();
    {
        //指定脚本路径
        redisScript2.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/redisScript2.lua")));
        // 指定返回类型
        redisScript2.setResultType(Boolean.class);
        // 
        keys.add("loopQueue");
        keys.add("waitingHash");
        argv.add("q1");
        argv.add("q2");
    }
public void createLoopQueue() {
        if (redisTemplate.execute(redisScript2, keys, argv.toArray())) {
            System.out.print("新建循环队列");
        } else {
            System.out.print("循环队列已存在");
        }
    }
步骤二:从缓存队列中获取数据——(lua脚本+redisTemplate)
1.定义脚本
local len = redis.call('LLEN', KEYS[1])
if len == 0 then
    return '-1' //缓存数据已用完毕
end

local keySet = redis.call('LINDEX', KEYS[1], 0)
local keySetLen = redis.call('LLEN', keySet)
if keySetLen == 0 then
    return redis.call('LPOP', KEYS[1]) //缓存队列数据已用完毕
else
    local value = redis.call('LPOP', keySet)
    redis.call('HMSET', KEYS[2], value,'0')
    return value //返回缓存中获取的数据
end

2.redisTemplate调用脚本——getByLoop
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
{
        // 指定 lua 脚本
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/redisScript.lua")));
        // 指定返回类型
        redisScript.setResultType(String.class);
      
 }
/**
     * 循环从缓存中获取数据
     *
     * @return 从缓存中或数据库(缓存穿透)获取的数据
     */
    public String getByLoop() {
        String resultFromLua = getResultFromLua();
        while (true) {
            if (resultFromLua.equals("-1")) {
                System.out.println("全部缓存已用完毕,从数据库获取数据");
                // -1 全部缓存已用完毕,从数据库获取数据
                resultFromLua = "mysql";
                break;
            } else if (resultFromLua.equals("q1")) {// q1缓存已用完->循环获取
                System.out.println("q1缓存已用完->循环获取");
                //填充缓存
                fillCache("q1");
                resultFromLua = getResultFromLua();
                continue;
            } else if (resultFromLua.equals("q2")) {// q2缓存已用完->循环获取
                //填充缓存
                System.out.println("q2缓存已用完->循环获取");
                fillCache("q2");
                resultFromLua = getResultFromLua();
                continue;
            }
            break;
        }
        System.out.println("获取数据=" + resultFromLua);
        //记录缓存中数据被消费的其他信息(当前时间戳-真实情况可能是用户信息)
        redisTemplate.opsForHash().put(keys.get(1), resultFromLua, String.valueOf(System.currentTimeMillis()));
        return resultFromLua;
    }

    /**
     * 脚本获取缓存数据
     *
     * @return -1:表示缓存中的数据已被消费完,q1或q2表示缓存队列被消费
     */
    private String getResultFromLua() {
        return String.valueOf(redisTemplate.execute(redisScript, keys, keys.toArray()));
    }

    /**
     * 异步填充数据
     *
     * @param q : which name of  queue
     */
    @Async
    void fillCache(String q) {
        // TODO 从数据库中获取数据
        Integer integer = Integer.valueOf(this.ss2[0]);
        this.ss2[0] = String.valueOf(integer + 1);
        //填充缓存数据
        redisTemplate.opsForList().leftPushAll(q, this.ss2);
        //从相反的方向push到维护队列中
        redisTemplate.opsForList().rightPush(keys.get(0), q);
        //TODO 通知数据刷新
    }

推荐阅读