Redis 分布式锁的实现
本文最后更新于 2024年6月7日 下午
前言
本文是阅读《Redis 实战》期间的学习笔记,书中的主要功能使用使用
Python
进行实现,本文使用Golang
对功能进行复现。
Redis为开发者们提供了事务的功能,具体的实现为:以特殊命令MULTI
开始,接着输入要执行的多条命令,最后输入特殊命令EXEC
标志着事物的结束,同时还需要配合WATCH
命令以保持数据的一致性,在WATCH
期间,当客户端执行EXEC
时,若Redis
检测到,有其他客户端抢先对WATCH
的数据进行了修改,则会通知当前客户端本次事务执行失败。所以说本质上,Redis
事务的数据一致性是通过借助锁来实现的,只不过这个锁本质上是一把乐观锁。
- 乐观锁:乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。
- 悲观锁:悲观锁,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。
- 共享锁:多个事务可以同时读取数据,但是不能修改数据。
- 排他锁:只有一个事务可以独占数据,其他事务需要阻塞等待。
这种乐观锁,由于在执行事务前并不会检测检测数据的一致性,当检测到数据的不一致时,本次事务执行失败,本次事务逻辑计算、网络传输时间的消耗也就失去了意义,因为还需要再一次对事务进行执行直到事务执行通过。当并发量上升时,无可避免地会发生多个事务同时修改同一数据的情况,这时会出现事务大量重复尝试的情况,导致平均每个事务的执行时间大幅度增加。
这种情况下只借助Redis中提供的Watch机制以达到乐观锁的功能,会对性能造成一定的负担,为了以更好的形式实现想要的功能,我们需要自己动手实现一个悲观锁,虽然多个事务同时读取同一数据时,也会造成多个事务反复尝试的情况,但是悲观锁的先获取锁的机制,节省了逻辑计算与网络传输的时间。
虽然在逻辑上,悲观锁在高并发情况下的性能理应更优,但是在我的2天的实际的测试中,由于测试方法的不恰当,导致最终的结果,并不如上文预料的那样,反而与之相反。
代码实现
Anyway!还是分析一下我们的代码实现吧。
业务场景
我们进行实验的业务场景为:“一个在线商店,可以供用户出售或者购买商品。”Redis中的数据结构如下:
- 用户信息:散列类型
- key:
<users> : <userId>
- value:
- name:
- funds:
- key:
- 用户背包:set类型
- key:
inventory : <sellerId>
- value:
<itemId>
- key:
- 市场:
zset
类型- key:
market:
- value:
<itemId> . <sellerId>
- 分值:
<价格>
- key:
Watch 方式实现的乐观锁
使用Watch
方式实现上架商品
1 |
|
使用Watch
方式实现购买商品
1 |
|
自己实现的悲观锁
Redis
中拥有的SetNX
命令,是set if not exist
的简写,因此当某个键值对不存在时使用该命令会设置键值,指定键对应的键值对存在时,该命令时会返回错误。这可以看作一个原子命令,可以用该命令实现获取锁的功能。
键值对的键名为锁的名字,键值对的值则为一个不会重复的UUID,并为该键值对设置一个过期时间。设置成功则意味着获取到锁。释放锁的操作,则由删除该键值对的操作实现。
虽然如上文所说,虽然在我们的测试中,通过SetNX
方式实现的悲观锁的性能不如Watch
,但并不意味着我们实现锁的方式是错误的。在此记录我们对锁的实现:
获取锁
1 |
|
释放锁
1 |
|
使用SetNX
方式实现上架商品
1 |
|
使用SetNX
方式实现购买商品
1 |
|