在高并发的使用场景下, 如何让 redis 里的数据尽量保持一致, 可以采用分布式锁以分布式锁的方式来保证对临界资源的互斥读写
redis 使用缓存作为分布式锁, 性能非常强劲, 在一些不错的硬件上, redis 可以每秒执行 10w 次, 内网延迟不超过 1ms, 足够满足绝大部分应用的锁定需求
redis 常用的分布式锁的实现方式:
一 setbit / getbit
用索引号为 0 的第一个比特位来表示锁定状态, 其中: 0 表示未获得锁, 1 表示已获得锁
优势: 简单;
劣势: 竞态条件(race condition), 死锁
获得锁的过程至少需要两步: 先 getbit 判断, 后 setbit 上锁由于不是原子操作, 因此可能存在竞态条件; 如果一个客户端使用 setbit 获取到锁, 然后没来得及释放 crash 掉了, 那么其他在等待的客户端将永远无法获得该锁, 进而形成了死锁所以这种形式不太适合实现分布式锁
二 setnx / del / getset
redis 官网有一篇文章专门谈论了实现分布式锁的话题基本的原则是: 采用 setnx 尝试获取锁并判断是否获得了锁, setnx 设置的值是它想占用锁的时间(预估):
如返回 1, 则该客户端获得锁, 把 lock.foo 的键值设置为时间值表示该键已被锁定, 该客户端最后可以通过 DEL lock.foo 来释放该锁
如返回 0, 表明该锁已被其他客户端取得, 这时我们可以先返回或进行重试等对方完成或等待锁超时
通过 del 删除 key 来释放锁某个想获得锁的客户端, 先采用 setnx 尝试获取锁, 如果获取失败了, 那么会通过 get 命令来获得锁的过期时间以判断该锁的占用是否过期如果跟当前时间对比, 发现过期, 那么先执行 del, 然后执行 setnx 获取锁如果整个流程就这样, 可能会产生死锁, 请参考下面的执行序列:
所以, 在高并发的场景下, 如果检测到锁过期, 不能简单地进行 del 并尝试通过 setnx 获得锁我们可以通过 getset 命令来避免这个问题来看看, 如果存在一个用户 user4, 它通过调用 getset 命令如何避免这种情况的发生:
getset 设置的过期时间跟上面的 setnx 设置的相同:
如果该命令返回的结果跟上一步通过 get 获得的过期时间一致, 则说明这两步之间, 没有新的客户端抢占了锁, 则该客户端即获得锁如果该命令返回的结果跟上一步通过 get 获得的过期时间不一致, 则该锁可能已被其他客户端抢先获得, 则本次获取锁失败
这种实现方式得益于 getset 命令的原子性, 从而有效得避免了竞态条件并且, 通过将比对锁的过期时间作为获取锁逻辑的一部分, 从而避免了死锁
三 setnx / del / expire
这是使用最多的实现方式: setnx 的目的同上, 用来实现尝试获取锁以及判断是否获取到锁的原子性, del 删除 key 来释放锁, 与上面不同的是, 使用 redis 自带的 expire 命令来防止死锁 (可能出现某个客户端获得了锁, 但是 crash 了, 永不释放导致死锁) 这算是一种比较简单但粗暴的实现方式: 因为, 不管实际的情况如何, 当你设置 expire 之后, 它一定会在那个时间点删除 key 如何当时某个客户端已获得了锁, 正在执行临界区内的代码, 但执行时间超过了 expire 的时间, 将会导致另一个正在竞争该锁的客户端也获得了该锁, 这个问题下面还会谈到
我们来看一下宿舍锁的简单实现很简单:
通过一个 while(true), 在当前线程上进行阻塞等待, 并通过一个计数器进行自减操作, 防止永久等待
百度搜索就爱阅读, 专业资料, 生活学习, 尽在就爱阅读网 92to.com, 您的在线图书馆!
来源: http://www.92to.com/bangong/2018/02-01/33247228.html