分布式锁

本文深入探讨了三种分布式锁实现方式:数据库乐观锁、Redis锁和ZooKeeper锁。详细解析了每种方式的工作原理,包括Redis锁的setnx、getset、expire和del指令,ZooKeeper的有序节点和事件监听特性,以及数据库乐观锁的版本号机制。讨论了各种实现的优缺点,如Redis锁的Redlock算法和数据库锁的性能问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分布式锁实现的3种方式:数据库乐观锁实现,Redis实现,zookeeper实现
分布式锁实现的关键是在分布式的应用服务器外,搭建一个存储服务器
要根据的具体业务场景选择技术方案。
1、Redis实现分布式锁
原理:
1.通过setnx(lock_timeout)实现,如果设置了锁返回1, 已经有值没有设置成功返回0
2.死锁问题:通过实践来判断是否过期,如果已经过期,获取到过期时间get(lockKey),然后getset(lock_timeout)判断是否和get相同,相同则证明已经加锁成功,因为可能导致多线程同时执行getset(lock_timeout)方法,这可能导致多线程都只需getset后,对于判断加锁成功的线程,再加expire(lockKey, LOCK_TIMEOUT, TimeUnit.MILLISECONDS)过期时间,防止多个线程同时叠加时间,导致锁时效时间翻倍
3.针对集群服务器时间不一致问题,可以调用redis的time()获取当前时间

1.setnx : 不能设置重复key
2.getset : 获取旧的值,设置新的值
3.expire : 设置key的有效期
4.del : 删除key

setnx(lockkey,currentTime+timeout)
lockkey : 就是key的名称
currentTime : 时间戳(System.currentTimeMillis())
timeout : 这个锁被动释放的时间,定义在配置文件中,方便修改
如果设置成功,也就是返回1,

expire(lockkey,timeout)给这个key设置有效期

del(lockkey)

如果设置失败,也就是返回0,代表当前有tomcat正在使用锁,还没有释放
那就获取当前锁
get(lockkey) 得到valueA
valueA!=null && currentTime (当前时间毫秒数)>valueA
代表这个key已经超时了, 这个时候获取到这个锁的tomcat有权重新设置超时时间,也就是重新设置value

getset(lockkey,currentTime+timeout) 得到valueB
执行完后 返回valueB,如果valueB ==null || valueA(之前get得到的值) == valueB
那么便是成功的获取到锁,执行获取到锁的流程

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

互斥性。在任意时刻,只有一个客户端能持有锁。
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

超时时间的设置,只是为了万一在逻辑执行过程中系统直接挂掉,没能在最后手动释放锁,又没有超时时间,就会导致该锁永远被占用。

Redis 分布式锁并不能解决超时问题,其实基于 ZooKeeper 实现的分布式锁也没办法避免超时问题。
考虑如下场景,加锁和解锁之间的业务非常耗时,那么就可能存在:
线程一拿到锁之后执行业务
还没执行完锁就超时过期了
线程二此时拿到锁乘虚而入,开始执行业务…

集群环境如何保证锁的安全
redis分布式锁在集群环境下,不是绝对的安全的。比如:主节点的锁还没来得及同步到从节点,此时主节点挂了,从节点取而代之。Redlock 算法就是为了解决这个问题,他的原理是在加锁时,向过半节点发送 set 指令,只要过半节点返回成功,那就认为加锁成功。释放锁时,再向所有节点发送 del 指令。随着集群机器的增加,势必会有损性能。Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题。
所以一般这种由于主从节点同步时间差导致的锁不安全问题,业务系统一般都是选择忍受的,生产上这种场景发生的概率也不大。

reids 分布式锁本质就是使用 setnx 指令实现占位功能,所以这种分布式锁是一种悲观锁,我们也可以借助 redis 的 watch 指令实现乐观锁。

实现原理
watch 会在事务开始之前盯住某个变量,当事务执行提交执行时,redis 会自动检查被watch的变量,是否被修改过了,如果变量被修改过,事务提交指令 exec 会返回 null 告知客户端事务执行失败。
当 exec 指令返回一个 null 时,客户端知道了事务执行是失败的。
multi/exec 对应数据库中的 begin/commit,discard 对应 rollback
Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键变量,否则会出错。

2、zookeeper分布式锁
zookeeper中的几个特性:
1)有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号,也就是说如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。
2)临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点
3)事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:1)节点创建;2)节点删除;3)节点数据修改;4)子节点变更

使用zookeeper实现分布式锁的算法流程,假设锁空间的根节点为/lock:
1.客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。

2.客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁;

3.执行业务代码;

4.完成业务流程后,删除对应的子节点释放锁。

3、数据库乐观锁
基于数据版本(version)的记录机制实现的,即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表添加一个 “version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。
在更新过程中,会对版本号进行比较,如果是一致的,没有发生改变,则会成功执行本次操作;如果版本号不一致,则会更新失败。

a. 先执行select操作查询当前数据的数据版本号,比如当前数据版本号是26:

select id, resource, state,version from t_resource where state=1 and id=5780;

b. 执行更新操作:

update t_resoure set state=2, version=27, update_time=now() where resource=xxxxxx and state=1 and version=26

c. 如果上述update语句真正更新影响到了一行数据,那就说明占位成功。如果没有更新影响到一行数据,则说明这个资源已经被别人占位了。

缺点:
(1). 这种操作方式,使原本一次的update操作,必须变为2次操作: select版本号一次;update一次。增加了数据库操作的次数。
(2). 如果业务场景中的一次业务流程中,多个资源都需要用保证数据一致性,那么如果全部使用基于数据库资源表的乐观锁,就要让每个资源都有一张资源表,这个在实际使用场景中肯定是无法满足的。而且这些都基于数据库操作,在高并发的要求下,对数据库连接的开销一定是无法忍受的。
(3). 乐观锁机制往往基于系统中的数据存储逻辑,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整,如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开。

排他锁(悲观锁)
通过数据库的排他锁来实现分布式锁
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过connection.commit()操作来释放锁

数据库实现分布式锁会有各种问题:
排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接;
操作数据库需要一定的开销,性能问题需要考虑;
MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值