分布锁一般有以下几种实现方式:数据库方式、redis、zookeeper 。
1、数据库方式
数据库方式,一般可以使用以下三种方式来实现
1.1 基于表记录方式
创建一张表,表中某个字段设置为unique,在需要锁时,就往表中新增一条记录,新增成功表示获取锁成功,新增失败表示获取锁失败。释放锁就删除这条记录。
总结:
- 这种锁没有失效时间,一旦释放锁失败,这条记录就会一直保存在数据库中,从而导致其他线程永远无法获取到锁。但可以启动一个定时任务定时去清理。
- 这种锁方式,不支持阻塞。因为插入失败会直接报错,要先获取锁,需要重新再次插入。可以弄个for循环或while循环,直到成功为止。
- 这种锁方式是不可重入的,已经获取锁的线程,再次插入也会报错。可以加入一些字段,比如线程信息,在再次获取锁时,可以先查询,如果新增的这些信息匹配,则可以获取锁。
1.2 数据库乐观锁
需要在表中新增字段,比如叫version,针对某个表中的一条数据,每次更新前,需要先将该条记录查询出来,同时对version字段值加1,然后更新时,where条件语句后带version条件,更新成功,则表示锁已被占用,更新失败,则表示锁未被占用。
总结:
- 该方式,不依赖于数据库本身的锁机制,不会影响请求性能,并发小的时候,只会有少数请求失败。
- 对表需要额外增加字段,增加了数据库的冗余度。当并发高时,会频繁修改version的值,从而给数据库造成压力,也会造成大量请求失败。
- 该方式,可用于并发量不大、并且写操作不频繁的情况下。
1.2 数据库悲观锁
该方式是数据库自带的,形如select… for update. 使用悲观锁时,MySql的InnoDB引擎时,表中必须有主键或索引才能使用行级锁(for update),否则会执行表级锁。
总结:
- 使用悲观锁,需要关闭自动提交功能
- 悲观锁,如果使用不当,可引发死锁
2、Redis方式
网上有很多关于使用Redis来实现分布式锁的说明,无非是用到setnx命令,抑或是SET EX PX NX命令,最后用到LUA脚本,来保证原子性。获取锁失败后,需要等待或重新尝试获取锁,浪费资源。感觉都不是很合理。后来发现有个Redisson框架,也可以实现分布式锁,该方式底层也是调用LUA脚本。
Redisson提供了锁续期功能,持有锁的线程,如果在锁键过期后,还没有执行完, 自动将锁键的时间延长。
只要线程A加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程A还持有锁,那么就会不断的延长锁key的生存时间。因此Redisson解决了锁过期释放,业务没执行完问题。
@Autowiredprivate RedissonClient redissonClient;public String lock() {// 获取锁RLock lock = redissonClient.getLock("lock"); //锁键值boolean acquired = lock.tryLock(10,-1,TimeUnit.SECONDS);if (acquired) {// 获取锁成功,执行业务逻辑return "获取锁成功,执行业务逻辑...";} else {// 获取锁失败,重试return "获取锁失败,重试...";}}public String unlock() {// 释放锁RLock lock = redissonClient.getLock("lock");lock.unlock();return "释放锁成功...";}
tryLock() 方法用于尝试获取分布式锁,该方法有三个参数:
- waitTime:等待获取锁的时间,单位为毫秒。
- leaseTime:锁的过期时间,单位为毫秒。
- 时间单位
waitTime 参数表示客户端最多等待多长时间来获取锁。如果在 waitTime 时间内没有获取到锁,则会返回 false。
leaseTime 参数表示锁的过期时间。如果锁在 leaseTime 时间内没有被释放,则会自动释放。如果 leaseTime 设置为 -1,则表示锁的过期时间由 renew() 方法来控制。这样,在业务逻辑执行过程中,可以定期调用 lock.renew() 方法来续期锁的过期时间。
tryLock() 方法的返回值是一个 Boolean 值,表示是否成功获取到锁。如果成功获取到锁,则返回 true。否则,返回 false
3、ZooKeeper方式
第一种方式,利用节点名称的唯一性来实现锁。加锁操作时,所有的客户端都创建同一个名字的节点,例如/tryLock/test,只有一个客户端能创建成功,可以获得锁。解锁时,只需要删除该节点即可。其余客户端再次竞争创建节点,直到所有的客户端都获得锁。
第二种方式,利用临时顺序节点实现锁。所有的客户端,都在/test-lock目录下创建临时顺序节点,如果客户端发现自身创建的节点序号是/lock目录下最小的那个节点序号,则获得锁。否则客户端监听比自己创建的节点序号小的最大的那个节点,并进入等待状态。
比如,同一时间,创建三个节点,/test-lock/01,/test-lock/02,/test-lock/03,那么创建01的客户端先获取锁,创建02的客户端监视01节点,创建03的客户端,监视02节点。当创建01的客户端完成并删除01节点时,会唤醒创建02的客户端。
总结:
- 第一种方式,会产生惊群效应,锁释放时,剩余的客户端都会再次竞争锁,但只有一个客户端能获取锁。
- 第二种方式,锁释放时,只有一个客户端被唤醒。
- 当ZooKeeper宕机时,临时顺序节点会被删除,获取锁的客户端会释放锁,不会造成一直等待锁。
具体实现可以参考:https://blog.csdn.net/weixin_41203765/article/details/141459764