redis缓存一致性
双写模式
并发情况
- 写缓存时间存在差异,导致缓存一段时间的脏数据,失效更新后最终一直
失效模式
并发问题
1写入db删除缓存 2写入db但未来得及删除缓存 3读取数据库读取的是1写入的数据,导致读取的并不是最新数据还是存在缓存不一致问题
解决方案
无论是双写还是失效模式,都一定程度上有缓存不一致问题,怎么办?
- 如果是用户维度数据(订单数据,用户数据),个人操作的数据并发几率小,不用考虑高并发问题,缓存数据设置过期时间,每个一段时间触发更新即可
- 如果是菜单,商品等介绍信息等基础数据,可以使用canal订阅binlog的方式(阿里优秀的解决方案)
- 缓存数据+过期时间能解决大部分业务对于缓存的要求
- 通过加锁保证并发读写,写写的时候阻塞顺序执行,读读无所谓,所以更适合读写锁,(业务不关心脏数据,允许临时的脏数据可以忽略)
总结
- 我们能放进缓存本就不应该是实时性的,一致要求高的,所以缓存数据的时候加上过期时间,保证最终一致性即可
- 不应该过度设计,增加系统复杂性
- 遇到实时性,一致性高的数据,查数据库,即使慢一点
redis分布式锁
redis 加锁
- 加锁
加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。
SET lock_key random_value NX PX 5000
值得注意的是:
random_value 是客户端生成的唯一的字符串。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。
- 解锁
解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来。
为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
redisson 加锁
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类
- 获取锁实例
RLock lock = client.getLock(“lock1”); 这句代码就是为了获取锁的实例,然后我们可以看到它返回的是一个RedissonLock对象。
- 加锁
当我们调用lock方法,定位到lockInterruptibly。在这里,完成了加锁的逻辑。
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
//当前线程ID
long threadId = Thread.currentThread().getId();
//尝试获取锁
Long ttl = tryAcquire(leaseTime, unit, threadId);
// 如果ttl为空,则证明获取锁成功
if (ttl == null) {
return;
}
//如果获取锁失败,则订阅到对应这个锁的channel
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
while (true) {
//再次尝试获取锁
ttl = tryAcquire(leaseTime, unit, threadId);
//ttl为空,说明成功获取锁,返回
if (ttl == null) {
break;
}
//ttl大于0 则等待ttl时间后继续尝试获取
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
//取消对channel的订阅
unsubscribe(future, threadId);
}
//get(lockAsync(leaseTime, unit));
}
-
流程如下
-
获取锁
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
//如果带有过期时间,则按照普通方式获取锁
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//先按照30秒的过期时间来执行获取锁的方法
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
//如果还持有这个锁,则开启定时任务不断刷新该锁的过期时间
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
Long ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}