46

这才是缓存Reload的正确姿势,你写对了吗?

 4 years ago
source link: https://www.tuicool.com/articles/INv63aE
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在上一篇文章《 Redis高级玩法:如何利用SortedSet实现多维度排序 》中我提到了一个排行榜设计方案:用一个JOB定时计算然后Reload热门榜单。这里涉及一个很好的细节问题,那就是如何Reload,怎么保证Reload过程中,其他请求不会有任何影响。

DEL+LPUSH

有位非常细心的朋友就提到了这个问题:Reload缓存如果是先删除再lpush批量插入,那么在删除和批量插入这个时间间隙,访问榜单的请求就会拿不到数据。时序图如下所示:

RBRrU3j.png!web DEL+LPUSH的问题

虽然DEL与LPUSH的时间间隔非常短,可能只有0.1ms,但是正是因为这0.1ms的时间间隙,导致你的实现方案并不完美,而我们的目标就是要做到 完美

pipeline

pipeline本质也是执行del+lpush两台命令,只不过客户端会先打包好这两条命令,然后再一起发送给Redis服务端执行。原理如下图所示:

bQ3IFzV.png!web pipeline原理

pipeline是被广泛使用的技术,并不是redis特有的,其主要意义就是节省了多个命令执行过程中的往返时间(Round Trip Time,即RTT),其对性能提升是非常显著的。例如,许多POP3协议实现已经支持此功能,大大加快了从服务器下载新电子邮件的过程。那么这种实现方式,会存在lrange访问不到数据的情况么?会。因为 pipeline机制只能优化吞吐量,但是无法提供原子性/事务保障 。这个得靠接下来介绍的eval或者multi+exec了。

multi+exec

既然谈到redis的原子性和事务,那就不得不说multi+exec了。这个组合命令也能保证执行的多个命令的原子性。以最常用的Redis客户端Jedis为例,有对其进行封装。通过multi+exec原子性执行del+lpush组合命令示例代码如下:

public class MultiExecTest {
    public static void main(String[] args) {
        try (JedisPool pool = new JedisPool(new JedisPoolConfig(), "192.168.0.1", 6379, 5000, "afei", 0);
             Jedis jedis = pool.getResource()) {
            String cacheKey = "HotApp";
            Transaction t = jedis.multi();
            t.del(cacheKey);
            t.lpush(cacheKey, "wechat", "alipay", "taobao", "qq", "tiktok");
            List<Object> result = t.exec();
            for (Object item:result){
                System.out.println(item);
            }

        }
    }
}

eval(推荐)

eval是redis用来保证原子性的命令。Redisson就是利用eval来实现分布式锁的。它加锁的源码如下:

<T> RFuture<T> tryLockInnerAsync(...) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    // 获取锁时需要在redis实例上执行的lua命令
    return commandExecutor.evalWriteAsync(... ,
              // 首先分布式锁的KEY不能存在,如果确实不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间(也是锁的租约时间)
              "if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              // 如果分布式锁的KEY已经存在,并且value也匹配,表示是当前线程持有的锁,那么重入次数加1,并且设置失效时间
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  "... ..." +
              // 获取分布式锁的KEY的失效时间毫秒数
              "return redis.call('pttl', KEYS[1]);",
              // 这三个参数分别对应KEYS[1],ARGV[1]和ARGV[2]
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

Redis 使用单个Lua 解释器去运行所有脚本,并且,Redis也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或Redis命令被执行 。这和使用MULTI/EXEC封装的事务很类似。在其他别的客户端看来,脚本的效果要么是不可见的,要么就是已完成的。

所以,我们只需要借鉴这段源码就能实现一个原子性的Reload操作,如下实现,del和lpush就是一个原子操作啦,完美:

return commandExecutor.evalWriteAsync(... ,
  "redis.call('del', KEYS[1]); " +
  "redis.call('lpush', KEYS[1], ARGV[1], ARGV[2], ARGV[3], ...); " + ,
  Collections.<Object>singletonList(KEY, list);

Reload总结

reload操作无论怎么实现,其关键就是Reload的步骤要是事务性的。以del+lpush组合命令reload榜单缓存数据为例,在del命令与lpush命令执行间隙,其他命令不能有任何执行的机会,这才是Reload操作是否足够健壮的关键。

【阿飞的博客】 公众号二维码

↓↓↓↓

UZ7B7n7.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK