1.缓存不一致的原因:
上图描述了通常情况下缓存不一致的原因:并发操作缓存的缓存丢失问题。
- t1 时刻 threadB 查询到缓存没有数值,于是去数据库查询到数值后进行set操作。
- t2时刻threadC 执行更新数据库操作,并在t3时刻进行了缓存删除(或者更新)
- t4时刻threadB将查询到的值设置到缓存中,注意 这个值将threadC更新的新值丢弃了导致的缓存的不一致。
2.解决方案概述
当前通用的数据缓存策略为,更新数据库后,设置缓存失效,下次查询的时候查询数据库更新缓存。这个策略的问题如除上图所述外,还有高热key缓存雪崩的问题。修改缓存策略,当数据库数据修改后,更新缓存数据使用SET操作,不使用DEL操作。即当数据库事务完成后,将新版本数据更新到缓存中,使用数据更新时间戳作为数据版本号更新缓存数据时,比较版本号,以此来防止过期数据的写入,同时使用原子性策略保证更新操作的原子性。
3.缓存更新原子性技术实现方案比较
3.1 基于关系型数据库本地事务的实现
使用本地事务主要是为了保证DML时,DB和redis操作的原子性
DML操作
- DML时,在同一个本地事务中,先操作DB,再操作redis
- 对于redis的操作基于lua实现,lua的作用是实现乐观锁
Select操作
- cache miss时,同样基于lua写入数据
优势
- 实现较为简单
- 基本解决数据不一致问题
缺点
- lua脚本会增加redis-server端的cpu使用率
- 随着lua脚本的膨胀,会造成redis-server端的慢查询等坏味道
- 膨胀了本地事务,造成行锁持续时间增长了一个redis的RT,同机房约200微秒
- 仍需设计redis降级方案,否则redis不可用时,整体服务也会不可用
3.2 先操作数据库,再删除缓存(适合修改集中的场景)
删除缓存操作也可改为set缓存操作,同样需要基于lua实现的乐观锁
DML操作
- 提交事务
- 删除缓存
Select操作
- 若cache miss,则写入缓存
优势
- 实现较为简单
- 性能较好
缺点
- 有小概率造成图一的场景,可通过以下方案控制不一致窗口期
- 缩短缓存TTL至可接受的最大不一致窗口大小
- 延迟删除策略,例如使用binglog作为延迟删除拖底方案
3.3 基于分布式锁的实现(适用于一致性要求较高的场景)
DML/Select 操作
- 获取分布式锁
- 操作DB
- 操作cache
- 释放分布式锁
优势
- 数据一致性等级较高(基于redis的redlock方案或redis的replication机制尽量保证一致性)
优势
- 实现成本略高
- Select cache miss时,读写完全串行化,性能损失较大
- 感觉不太适合咱们的场景
3.4 基于原子性lua脚本&数据版本的无锁缓存更新方式
数据更新时,强制更新数据版本号(version)字段,set version=version+1
数据更新事务提交完成后,基于版本号乐观锁,原子性更新缓存,lua脚本如下:
/**
* 缓存 value field
*/
private static final String ORDER_CACHE_VALUE_FIELD = "value";
/**
* 缓存 version field
*/
private static final String ORDER_CACHE_VERSION_FIELD = "version";
/**
* 数据原子更新 lua 脚本
*/
private static final String script =
"local ver = redis.call('hget', KEYS[1], '" + CACHE_VERSION_FIELD + "') "
//版本号判断
+ "if ver ~= false and tonumber(ver) >= tonumber(ARGV[1]) then "
+ "return 0 "
+ "else "
+ "redis.call('hset', KEYS[1], '" + CACHE_VERSION_FIELD + "', ARGV[1]) "
+ "redis.call('hset', KEYS[1], '" + CACHE_VALUE_FIELD + "', ARGV[2]) "
+ "redis.call('expire', KEYS[1], ARGV[3]) "
+ "return 1 "
+ "end";
查询&或者更新场景下的缓存更新都是用基于数据版本号的无锁原子更新。
优势:
数据一致性高且并发友好。
优势:
方案实施难度较高,需要数据&缓存结构改造
4. 总结
- 要根据不同模型设置不同 TTL
- 核心订单模型的TTL、缓存过期方案选择,需要进一步理解订单update流程并进行命中率观察后再确定
- 订单update是否集中
- 订单频繁update时,读操作是否频繁
- 根据业务场景判断使用方案