常见的分布式锁解决方案

  1. 基于MySQL的悲观锁、乐观锁
  2. 基于redis的分布式锁
  3. 基于zookeeper的分布式锁

基于MySQL的悲观锁、乐观锁

悲观锁

悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的

20230717171343

MySQL for update的加锁:

# 向mysql申请一把锁 for update, 使用for update 的时候注意,默认每个语句mysql都是默认提交
# 需要关闭@@autocommit
select @@autocommit;
set autocommit=0;
select @@autocommit;
select * from inventory where goods=420 for update;
# 具体业务
# 释放锁
commit;

#这是一个行锁
#明确查询条件,就锁住指定数据 && 必须有索引,不然升级成为表锁

Gorm 的 for update锁:

//添加锁	
if result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where(&model.Inventory{Goods: goodInfo.GoodsID}).First(&inv); result.RowsAffected == 0 {
tx.Rollback() // 回滚
return nil, status.Errorf(codes.InvalidArgument, "没有库存信息")
}

乐观锁

特点:乐观锁是一种并发类型的锁,其本身不对数据进行加锁通而是通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,这种方式因为节省了悲观锁加锁的操作,所以可以一定程度的的提高操作的性能,不过在并发非常高的情况下,会导致大量的请求冲突,冲突导致大部分操作无功而返而浪费资源,所以在高并发的场景下,乐观锁的性能却反而不如悲观锁。

20230717205722

Gorm 实现乐观锁:

// 没有select 会出现gorm 忽略零值问题
db.Modle(&Inventory.{}).select("Stocks","Version").where("goods=? and version=?",goodInfo.GoodsId,inv.Version).Updates(Inventory{Stocks:inv.Stocks,Vesion:inv.Version+1})

基于redis的分布式锁

redsync

package main

import (
goredislib "github.com/redis/go-redis/v9"
"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
)

func main() {
// Create a pool with go-redis (or redigo) which is the pool redisync will
// use while communicating with Redis. This can also be any pool that
// implements the `redis.Pool` interface.
client := goredislib.NewClient(&goredislib.Options{
Addr: "localhost:6379",
})
pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)

// Create an instance of redisync to be used to obtain a mutual exclusion
// lock.
rs := redsync.New(pool)

// Obtain a new mutex by using the same name for all instances wanting the
// same lock.
mutexname := "my-global-mutex"
mutex := rs.NewMutex(mutexname)

// Obtain a lock for our given mutex. After this is successful, no one else
// can obtain the same lock (the same mutex name) until we unlock it.
if err := mutex.Lock(); err != nil {
panic(err)
}

// Do your work that requires the lock.

// Release the lock so other processes or threads can obtain a lock.
if ok, err := mutex.Unlock(); !ok || err != nil {
panic("unlock failed")
}
}

redsync的源码解读

  1. setnx的作用:将获取和设置值变成原子性操作

  2. 服务在删除之前挂了–>设置过期时间,防止死锁出现

    • 设置了过期时间,但是过期时间到了,业务逻辑还在执行怎么办–>在过期之前,刷新一下

    • 需要自己启动协程,来完成延时操作

      • 延时接口可能带来负面影响,如果其中一个服务hung住了,2s就能执行完,但是你hung住了那么你就会一直去申请延长锁,导致别人永远不能获取到锁
  3. 分布式锁需要解决的问题 - lua脚本-》原子性

    • 互斥性 - setnx
    • 死锁
    • 安全性
      • 锁只能被持有该锁的用户删除,不能被其他用户删除
        • 当时设置的value 是多少,只有g知道
        • 在删除的时候 去取出redis的值和当前自己保存的值对比

Redlock实现

antirez提出的redlock算法大概是这样的:

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。

为了取到锁,客户端应该执行以下操作:

  • 获取当前Unix时间,以毫秒为单位。
  • 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。
  • 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功
  • 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  • 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

基于Zookeeper实现分布式锁

  基于以上两种实现方式,有了基于zookeeper实现分布式锁的方案。由于zookeeper有以下特点:

  1️⃣维护了一个有层次的数据节点,类似文件系统。

  2️⃣有以下数据节点:临时节点、持久节点、临时有序节点**(分布式锁实现基于的数据节点)**、持久有序节点。

  3️⃣zookeeper可以和client客户端通过心跳的机制保持长连接,如果客户端链接zookeeper创建了一个临时节点,那么这个客户端与zookeeper断开连接后会自动删除。

  4️⃣zookeeper的节点上可以注册上用户事件(自定义),如果节点数据删除等事件都可以触发自定义事件。

  5️⃣zookeeper保持了统一视图,各服务对于状态信息获取满足一致性。

  Zookeeper的每一个节点,都是一个天然的顺序发号器。

  在每一个节点下面创建子节点时,只要选择的创建类型是有序(EPHEMERAL_SEQUENTIAL 临时有序或者PERSISTENT_SEQUENTIAL 永久有序)类型,那么,新的子节点后面,会加上一个次序编号。这个次序编号,是上一个生成的次序编号加一

  比如,创建一个用于发号的节点“/test/lock”,然后以他为父亲节点,可以在这个父节点下面创建相同前缀的子节点,假定相同的前缀为“/test/lock/seq-”,在创建子节点时,同时指明是有序类型。如果是第一个创建的子节点,那么生成的子节点为/test/lock/seq-0000000000,下一个节点则为/test/lock/seq-0000000001,依次类推,等等。