分布式锁
多个系统同时操作一个redis,因为jvm锁是线程级别的,所以没有办法锁住多个系统。
Redis锁实现:
- setnx key value 只有在key不存在时设置key的值
- 此时key相当于锁的唯一标识,若key存在则会返回失败
- EXPIRE key timeout 设置带过期时间的key,动态设置
- 保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住(万一拿到锁之后挂了就不会被显式释放)。
SETNX 和 EXPIRE 非原子性
- 如果 SETNX 成功,在设置锁超时时间后,服务器挂掉、重启或网络问题等,导致 EXPIRE 命令没有执行,锁没有设置超时时间变成死锁。
- 使用 lua 脚本保证原子性
锁误解除
本质上就是redis自动删除锁和主动删除锁的重复冲突
如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。
- 在 value 中设置一个 UUID 标识当前线程
- 每次解锁前判断一下锁的value是否为当前线程的UUID
- 获取锁的value和删除锁通过Lua脚本变成原子操作
超时解锁导致并发
如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。
解决方案:
- 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
- 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间
- 保证锁不会自动过期自动删除,就算程序挂了,依然是有过期时间不会造成死锁。
- 这个线程必须是主线程的守护线程,如果主线程挂了,定时任务也会挂了
锁等待
- 可以通过客户端轮询的方式解决该问题,当未获取到锁时,等待一段时间重新获取锁,直到成功获取锁或等待超时。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。
- 另一种方式是使用 Redis 的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。
整理来源
分布式锁的实现之 redis 篇
《吊打面试官》系列-Redis终章_凛冬将至 FPX_新王登基
RedLock
基于 Redis 的分布式锁到底安全吗? 待整理
Redis与Mysql双写一致性方案解析
根据缓存是删除还是更新,以及操作顺序大概是可以分为下面四种情况
- 先更新数据库,再更新缓存
- 先更新缓存,再更新数据库
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
更新缓存 VS 删除缓存
- 更新缓存的优点: 缓存不会增加一次Miss,命中率高
- 删除缓存的优点: 操作简单,能防止更新出现的线程安全问题
通常场景下删除缓存操作简单,并且带来的副作用只是增加了一次Cache Miss,建议作为通用的处理方式,因为:
- 更新缓存的代价可能很高,因为可能掺杂复杂的数据计算,而且频繁的更新缓存并不代表它会被频繁的访问到。举个栗子:一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。
- 删除缓存,而不是更新缓存,就是一个 Lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。
Redis与数据库一致性 | 笔记 详情