• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

风子sama

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

分布式锁

分布式

分布式的 CAP 理论告诉我们:任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

分布式锁

当在分布式模型下,数据只有一份,此时利用锁的技术控制某一时刻修改数据的进程数
与单机模式下锁不仅需要保证进程的可见性,还需要考虑进程与锁之间的网络问题,
分布式锁还可以将标记存在内存,值是该内存不是某个进程分配的内存而是公共内存如redis。利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行

设计分布式锁

可以保证在分布式集群部署的应用集群中,同一个方法在同一时间只能呗一台机器上的一个线程执行
这把锁是一把可重入锁(避免死锁)
这把锁最好是一把阻塞锁(根据业务考虑)
这把锁最好是一把公平锁(根据业务考虑)
有高可能的获取和释放锁的功能
获取锁和释放锁的性能要好

基于数据库做分布式锁

乐观锁

基于表的主键唯一做分布式锁
思路:利用主键唯一的特性,如果有多个请求同时提交到数据库的话,数据库只会保证一个操作可以成功,那么我们就可以认为操作成功的哪个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁,删除这条数据库记录即可

上面简单的实现有以下几个问题:
这把锁的强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,导致业务系统不可用
这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一致在数据库中,其他线程无法获取锁
这把锁只能是非阻塞的,因为数据insert操作,一旦插入失败就会直接报错,没有获得锁的线程并不会进入队列,想要再次获得锁就要再次出发获得锁的操作
这把锁是非重入锁,同一个线程无法在释放之前再次获得该所
在mysql数据库中采用主键冲突防重,在大并发情况下可能会造成级锁现象

表字段版本号

这个策略源于 mysql 的 mvcc 机制,使用这个策略其实本身没有什么问题,唯一的问题就是对数据表侵入较大,我们要为每个表设计一个版本号字段,然后写一条判断 sql 每次进行判断,增加了数据库操作的次数,在高并发的要求下,对数据库连接的开销也是无法忍受的。

基于悲观锁

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意: InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给要执行的方法字段名添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

基于 Redis 做分布式锁

基于 Redis 的 SETNX()、EXPIRE() 方法做分布式锁

setnx()
setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。

expire()
expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。

使用步骤
1、setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功
2、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。
3、执行完业务代码后,可以通过 delete 命令删除 key。

这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。比如,如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用 redis 的 setnx()、get() 和 getset() 方法来实现分布式锁。

基于 REDLOCK 做分布式锁

Redlock 是 Redis 的作者 antirez 给出的集群模式的 Redis 分布式锁,它基于 N 个完全独立的 Redis 节点(通常情况下 N 可以设置成 5)。

算法的步骤如下:
- 1、客户端获取当前时间,以毫秒为单位。
- 2、客户端尝试获取 N 个节点的锁,(每个节点获取锁的方式和前面说的缓存锁一样),N 个节点以相同的 key 和 value 获取锁。客户端需要设置接口访问超时,接口超时时间需要远远小于锁超时时间,比如锁自动释放的时间是 10s,那么接口超时大概设置 5-50ms。这样可以在有 redis 节点宕机后,访问该节点时能尽快超时,而减小锁的正常使用。
- 3、客户端计算在获得锁的时候花费了多少时间,方法是用当前时间减去在步骤一获取的时间,只有客户端获得了超过 3 个节点的锁,而且获取锁的时间小于锁的超时时间,客户端才获得了分布式锁。
- 4、客户端获取的锁的时间为设置的锁超时时间减去步骤三计算出的获取锁花费时间。
- 5、如果客户端获取锁失败了,客户端会依次删除所有的锁。

使用 Redlock 算法,可以保证在挂掉最多 2 个节点的时候,分布式锁服务仍然能工作,这相比之前的数据库锁和缓存锁大大提高了可用性,由于 redis 的高效性能,分布式缓存锁性能并不比数据库锁差。

基于 ZooKeeper 做分布式锁

ZOOKEEPER 锁相关基础知识

- zk 一般由多个节点构成(单数),采用 zab 一致性协议。因此可以将 zk 看成一个单点结构,对其修改数据其内部自动将所有节点数据进行修改而后才提供查询服务。
- zk 的数据以目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。
- 子节点有三种类型。序列化节点,每在该节点下增加一个节点自动给该节点的名称上自增。临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除。最后就是普通节点。
- Watch 机制,client 可以监控每个节点的变化,当产生变化会给 client 产生一个事件。

ZK 基本锁


- 原理:利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
- 缺点:所有取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后所有等待进程一起来创建节点,并发量很大。

ZK 锁优化


- 原理:上锁改为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平锁)。

步骤:
- 1.在 /lock 节点下创建一个有序临时节点 (EPHEMERAL_SEQUENTIAL)。
- 2.判断创建的节点序号是否最小,如果是最小则获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。
- 3.当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。
- 4.取锁成功则执行代码,最后释放锁(删除该节点)。

posted on 2021-09-26 10:31  风子sama  阅读(1346)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3