feling.net/_posts/2021-12-09-exquisite-redis.md

6.6 KiB
Raw Blame History

layout title categories tags
pages redis 的调用可以再精致点 杂谈
middleware
redis

分布式锁


消息折叠

“折叠” 这个词也是回福州工作后才接触的概念。用得太久以至于现在如果让我重新给它起个名字都毫无灵感了。具体的场景例子是这样的在课程业务中学习进度的计算是一个实时性不太敏感计算量又很重的逻辑。但前面触发进度计算的地方隐匿在系统各处恨不得一秒触发一百次进度百分比的更新。好在中间的位置有个收口大家都是通过同一个MQ消息触发的进度计算。于是在消息的生产者这边加了折叠逻辑 也就是同一个学员同一个课程的进度计算,一分钟内只发送一条。多余的消息给它吞掉(忽略)。

实现的原理, 就是用 userId + courseId 作为key给它上个redis分布式锁。同时把消息也改成延时一分钟的。

上锁的代码是五年前的人写的。某一天我们压测过程中发现有个毛刺一分钟一根就是来自这个MQ消息。打开代码一看。。哎。。 get()、set() 分开调用的,真的是批哩一点的并发概念都没有啊。

像这种地方,改成 setnx() 也就够了。但自己心里要清楚,只从技术角度看,分布式锁的实现还可以再深入很多步,做得更精致。如果有条件,应该把这类代码放入公共包里给更高级的开发人员维护。

redission

我一直是反感 redission 的(同样反感 spring-data恰恰是因为它的 开源、活力、长期的积累。导致它的模型复杂而庞大。它实现了完整的 jdk 定义的锁的接口。

我一直期待,遇到一个场景,是下面这三个接口方法不够用的。

lock()
tryLock()
unlock()

何必引入完整的jdk接口实现呢团队里的人都那么牛逼hold得住嘛

这天时间大概在一年一度的组织架构调整期间收到一个原本是其他小组维护的服务组织架构调整调过来的代码来了但人没来。里面有段bug是这样的。

RLock xLock = null, xxLock = null;
try {
    xLock = redissonClient.getFairLock("lockKey0");
    xLock.lock(10, TimeUnit.SECONDS);
    ...
    xLock.unlock()
    ...
    xxLock = redissonClient.getFairLock("lockKey1");
    xxLock.lock(10, TimeUnit.SECONDS);
    ...
    xxLock.unlock()
    ...
} catch (Throwable e) {
    ...
} finally {
    if (null != xLock && xLock.isLocked()) {
        xLock.unlock();
    } 
    if (null != xxLock && xxLock.isLocked()) {
        xxLock.unlock();
    }  
}

它试图用 xxLock.isLocked() 判断 “自己是否加锁成功了”,但 xxLock.isLocked() 却完全不是这个语义,被别人锁着也是 true。

这是一位应届生职级的同学可能还未系统学习过jdk里的并发包心还大到敢直接顾名思义也没有点进去看源码注释的习惯。编码习惯也不好一边希望减小锁的粒度混在业务代码中途调 unLock(),一边又把 try catch 的范围放到最大。

击穿


击穿的意思, 是 @cacheable

穿透


雪崩


keys *


但凡是有点自主学习能力,看过官网的。。。命令介绍里那么黑的加粗说明。

以一己之力发明了MemoryCache


跟前面讲的消息折叠里遇到的bug一样 也是五六年前的老代码了。顺带跑个题讲讲一个OOM的事情吧。那个服务大概启动一两周左右内存就满了fgc也回收不掉只能重启。

把内存dump下来看到占内存最多的除了日志打印就都是同一个sql语句的缓存。特点是 where 条件里包含 in (?, ? ...) , 这里面的 ? 可能有上万个, 而且不是固定数量。

ORM 框架会把sql语句缓存下来但是因为 ? 的个数有几万种可能,就缓存了几万遍。

ORM 框架是动不了了,在用的低版本的没法处理这个问题, 又不敢升级版本。保底咱还能把 ? 分批多执行几次sql至少让内存不爆是吧。但我又挣扎了一下想看看完整的业务逻辑感觉不会有什么功能是必须要用这么奇葩的sql实现的。结果就发现相关的数据库操作明显有优化过一波的痕迹mysql前面加过一层redis确认这个sql查询是没删干净的直接删了就完事。

回到正题,"mysql前面加过一层redis" 它是怎么加的呢,业务逻辑已经记不清了, 但我永远忘不了,它把 list 类型的数据整个 get() 出来, 添加一个元素,再 set() 回去。

缓存与DB的数据一致性


有这么个不解之谜,缓存里的内容和数据库中的数据不一致了。并且这部分的代码很新,很清晰。

读接口用的 @Cacheable 注解,实现了先从缓存取, 如果缓存里没有再走数据库,还会把数据库里取到的放进缓存里。

写接口用的 @CacheEvict 注解,实现了修改完数据库之后删除缓存。

已经是相对较优的实现了再追求更强的一致性就得去权衡代价和收益了。最终这个bug是被切了无法复现手动清下缓存改小了缓存有效期 就算结了。

咱们从理论上猜一下可能有哪些场景会导致数据不一致。

  1. 数据库主从延迟。写接口更新了主库,删除了缓存。读接口从从库里取出来的还是旧数据,并把旧数据又放进了缓存。
  2. 读接口 发现缓存里没数据, 走到数据库里去取,取完卡了一下,还没来得及放进缓存。写接口 更新完数据,删完缓存。读接口 从卡住的状态恢复过来,把旧数据放进了缓存。

关键的因素有两个,这两个因素构成了主要矛盾。

一是 读写两个接口之间的通信,只通过缓存的有无来交换信息,信息量不够。

二是 把缓存的修改操作分给了两个接口,这就需要更多信息量的交换才能协作。

我们不好去完全断开两个接口之间的通信与协作, 因为除了数据的新旧,他们还在沟通哪些数据应该进缓存。

所以就只好增加信息量,

  1. 写接口可以多说点假设数据中有version字段表示数据的版本。写接口 数据更新时删除缓存的主体内容但保留最新的version值。读接口就能根据版本号判断要放进缓存的是不是旧数据。

  2. (读接口,也要有主观能动性)。时不时的抽查下数据质量,不能说缓存里有数据就 100% 认为是合格的数据。那检查抽取样本的标准呢...(哎...越搞越复杂了)


  • 文章目录 {:toc}