分布式锁1-分布式锁实现的三种方式

分布式锁概念

为什么使用分布式锁

假设有这样一个场景,双十一抢iphone15ProMax手机场景,可以抢多台。操作数据库接口如下:

	void reduceInventory(Long id,int count) {
                //1.拿到数量信息
		Product product = mapper.selectById(id);
                //2.修改数量信息
		int newCount = product.getProductCount() - count;
		product.setProductCount(newCount);
		int i = mapper.updateById(product);
		System.out.println("修改成功!->" +i);
	}

在单个应用情况下可以使用synchronized或者lock锁解决并发问题,因为只有这一个应用可以调用这个接口。那么在微服务应用下呢?多个请求
同时访问这个接口,步骤1是可以同时执行的,在同一时间可能拿到的数量是相同的;假设A服务和B服务各有一个请求访问这个接口,那么A应用要抢
5个,B应用要抢1个,这样就导致A应用要更新数据库的时候数量是95,B应用要更新的数量是99,不管谁先拿到数据库的锁,都会后拿到数据库锁的
覆盖掉前面的数据,导致数据不正确。那么如何解决这个问题呢?答案就是分布式锁。

加分布式锁的本质就是保证步骤1和步骤2这个逻辑,或者说这块代码无论有多少个并发请求必须保证同步执行。而且,无论多少个服务都能通过
一个公用的组件来控制代码同步执行,比如MySQL、Redis、zookeeper。任何一个服务都能访问到它,而不是通过synchronized或者lock锁,
因为每个服务的synchronized或者lock锁都是只有当前服务能访问,不能实现服务之间的排他性。

分布式锁有三种实现方式

1. MySQL实现分布式锁

原理:

在数据库层面加锁处理,在一个线程修改数据查询数据的时候进行for update处理。这样其他线程在查询数据的时候就会阻塞,必须等当前
线程处理完才能执行for update查询语句。

步骤:

  1. 开启事务
  2. 执行select * from table where id = ? for update语句
  3. 提交事务

问题:

一旦有某个线程处理逻辑时开启了事务,但是抛出异常了,那么就会导致事务无法提交,其他线程永远拿不到数据库锁,导致死锁问题。

2.Redis+RUA脚本实现分布式锁

原理:

  1. redis中有nx 和 ex操作,其中nx表示当前key不存在时才执行成功,返回0表示失败,返回1表示成功;ex表示过期时间。
  2. 当加锁的时候执行nx操作,如果当前有线程持有锁则设置失败返回0,否则成功返回1.
  3. 释放锁的时候执行del操作。
  4. ex可以防止死锁问题,如果有个线程执行setnx操作了由于异常不执行del释放锁操作,那么其他线程永远无法拿到锁。

要解决的问题:

  1. 如何避免死锁问题:

设置ex过期时间。

  1. 如何保证锁不会被其他线程释放:

通过redis存储的value来控制,将value作为线程的唯一标识,只有当前线程的解锁标识和设置的value相同时才能删除当前key。

  1. 如何保证在释放锁时如何保证查询value值、必对value值、删除key操作的原子性

使用RUA脚本:f(redis.call(‘get’, KEYS[1]) == ARGV[1]) then return redis.call(‘del’, KEYS[1]) end return 0;传入
客户端的唯一标识和redis拿到的value对比,如果相同则删除key,否则不删除。

  1. 如何评估redis中key的过期时间

使用守护线程:

  1. 守护线程的特点是,当主线程销毁时,守护线程随机销毁
  2. 在加锁的时候,开启一个守护线程。
  3. 守护线程的操作是,在key将要失效的时候进行续期操作。
  4. 这样做的好处是在解决了死锁问题的同时,如果业务逻辑执行比较慢的话也不会导致锁自动失效的问题
  1. Redis宕机问题

假设一个客户端在加锁的时候,master实例突然宕机,没有向slave同步数据,其中一个slave升级为master,另一个实例重新获取锁,这种情况下锁的安全性被打破。

redis作者提出RedLock概念:

  1. 客户端记录当前时间戳T1,并设置锁的TTL时间
  2. 在服务器创建5个redis实例,非集群,不存在数据同步问题。
  3. 依次从5个redis实例去获取锁,相同的key-value,获取锁时要设置网络连接和响应的超时时间,该时间要小于锁TTL时间,避免客户端死等。
  4. 客户端记录最后一个获取锁成功的时间戳(因为有可能在五个Redis中有宕机发生就获取锁失败了)T2,和获取锁成功的个数n,其中n要大于等于3(半数以上节点获取重构)且T2-T1要小于TTL才算获取成功。
  5. 锁的时间为TTL - 取锁的时间;
  6. 如果加锁失败,客户端向所有Redis实例发送解锁请求。

其中有三个要解决的问题(NPC):

  1. Network Delay: 网络延迟问题
  2. Process Pause: 进程暂停问题
  3. Clock Drift: 时钟漂移问题(客户端服务器和Redis服务器时间不一致)

3. Zookeeper实现分布式锁

Zookeeper分布式锁时基于临时顺序节点实现的.
要解决的问题:

  1. 如何实现互斥性:Zookeeper中的节点类似文件系统的目录结构,当对其中一个节点加锁,其他线程就不能再创建了。
  2. 如何加锁解锁:当客户端连接Zookeeper后创建节点,断开连接删除节点
  3. 如何解决高可用问题:使用顺序节点+watch机制,当客户端加锁时会在目录下创建一个字子点并记录创建顺序节点,然后判断创建的顺序节点是否为第一个节点,如果是,加锁成功,如果不是watch上一个节点以此类推,
    如果加锁成功的客户端断开连接,那么监听该顺序节点的客户端就可以去获取锁,这样就减轻了高并发下的zookeeper压力,实现高可用。

实现原理:

  1. 创建临时顺序节点:每个客户端访问zk会创建临时节点,(例如/locks)下创建一个临时顺序节点。
  2. 获取所有子节点并排序:客户端通过getChildren获取/locks下的所有子节点,并对这些子节点按照它们的序号进行排序。
  3. 判断锁的拥有者:客户端检查它创建的节点是不是排序后的第一个节点,如果是,则获取到锁;如果不是,则监视排序后的前一个节点。
  4. 等待或者阻塞:如果这个节点不是第一个,客户端就会注册对前一个节点的监视器,并进入等待状态,直到前一个节点被删除释放锁。
  5. 释放锁:当客户端完成操作,释放锁时,会删除它创建的临时节点,然后通知监视器上的等待节点。
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值