redis分布式锁的原理与实现【分布式】


前言

一、什么是分布式锁

1、原理

分布式锁是指在分布式系统中,为了实现协调和同步访问共享资源,而对分布式环境下的多个进程或线程进行同步的一种机制。它可以保证在分布式环境下各进程访问共享资源的时序一致性和互斥性,避免不同进程之间发生冲突。
常见的分布式锁实现方式有以下几种:

基于数据库的分布式锁:使用数据库的事务机制来实现分布式锁,通过在数据库插入一个唯一的记录来实现锁定,其他进程尝试并发获取锁时会阻塞等待。

基于缓存的分布式锁:利用缓存服务器来实现锁,比如使用 Redis 的 SETNX 或者 RedLock 算法来实现分布式锁,其中 RedLock 是一种多重锁定方式,能够在不同节点之间避免竞争条件。

基于 ZooKeeper 的分布式锁:使用 ZooKeeper 集群节点来实现分布式锁,ZooKeeper 提供了顺序节点以及 watch 机制来帮助实现分布式锁的获取和释放

2、场景

举个例子来说明分布式锁的应用场景:假设某个电商网站的秒杀活动每天只有固定的5000件特价商品,多个用户同时尝试秒杀商品时会引发超卖问题,需要使用分布式锁来解决。在多个用户尝试秒杀特价商品时,他们所在的应用进程都要向 Redis 缓存服务请求获取锁,只有一个请求可以成功获取到锁并执行秒杀操作,其他请求则会被阻塞。在执行完秒杀操作后,该进程会释放锁,使得其他请求可以获取锁并继续秒杀操作,从而避免了超卖问题的发生。这种场景下,分布式锁能够保证秒杀操作的互斥性和时序一致性,保障秒杀活动的公正性。

二、redis实现分布式锁

1、redis实现分布式锁原理

由于 Redis 是单线程的,可以保证 SETNX 命令与 DEL 命令的原子性操作,因此可以通过 Redis 的 SETNX 命令实现分布式锁。在分布式系统中,要保证同一时间只有一个客户端可以访问某个共享资源,因此需要使用分布式锁来协调各个客户端的访问。Redis 分布式锁实现简单、灵活,只需要使用 SETNX 命令设置锁,并设置有效期即可,无需复杂的代码实现。redis 支持的编程语言非常多,比如 Python、Java、Go、PHP 等,这就意味着可以在不同语言的应用程序中使用同样的锁来管理共享资源,从而避免了不同编程语言之间的锁管理差异性问题。

2、Lock函数的实现

通过 Redis 的 SET 命令设置了一个锁。获取 Redis 分布式锁时采用了一个基于协程和 select 的非阻塞性等待方式
具体步骤如下:
1、获取 Redis 连接池中的一个连接 conn。
2、使用 channel ch 和超时通道 timeoutCh 进行非阻塞性等待获取锁。
3、将要获取锁的操作放在一个协程中执行(即 go func(){}()),用于不断地轮询 Redis,尝试获取锁。
4、通过 select 监听 ch 和 timeoutCh 两个 channel 上的数据。
5、如果 ch 上有数据(即成功获取到锁),则返回 true。
6、如果 timeoutCh 上有数据(即超时),则记录日志并返回 false。
在上面获取锁的步骤中:
SET 命令用于设置 Redis 键值对。lockKey 是锁的键名,前缀"lock:"是为了与其他键进行区分。
“1” 是锁的值,可以是任意值,只要保证不与其他进程的值冲突即可。
“NX” 表示只在键不存在时才执行设置操作。
“EX 10” 表示设置键的过期时间为10秒,这样即使持有锁的进程崩溃或异常退出,其它进程也能够及时再次获取锁。
该函数采用了 Redis 的 SET 命令中的 NX(Not eXists)选项,只有当键不存在时才执行设置操作。这样保证了多个进程之间只有一个能够成功地获取到这个锁,即“抢到锁的进程会关闭 锁通道并停止运行,因为已经获得了锁。其余等待的进程会在锁超时或者释放锁的时候后继续执行,并再次尝试获取锁。

// 加锁封装函数
func (m *UserModel) lock(key string, timeout time.Duration) bool {
    conn := m.Pool.Get()
    defer conn.Close()

    // 设置管道和超时通道
    ch := make(chan bool, 1)
    timeoutCh := time.After(timeout)

    // SET key value [EX seconds] [PX milliseconds] [NX|XX]
    // NX - 只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
    // PX - 毫秒为单位设置 key 的过期时间。 SET key value PX 1000 等效于 PSETEX key 1000 value
    // EX - 秒为单位设置 key 的过期时间。等效于 SET key value EX seconds 的效果。
    lockKey := fmt.Sprintf("lock:%s", key)
    go func() {
        for {
            // 获取锁
            res, err := redis.String(conn.Do("SET", lockKey, "1", "NX", "EX", "10"))
            if err == nil && res == "OK" {
                ch <- true
                return
            }
            time.Sleep(100 * time.Millisecond)
        }
    }()

    // 用 select 监听两个通道,任意一个有数据就返回
    select {
    case <-ch:
        return true
    case <-timeoutCh:
        log.Println("Failed to acquire lock after ", timeout, "s")
        return false
    }
}

3、实际使用

首先要连接线程池

	pool := &redis.Pool{
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", "localhost:6379")
		},
	}

成功获取到了锁之后,defer 关键字后的语句会在函数执行完毕后自动调用,此处实现了在解锁时删除锁键,即通过 DEL 命令删除锁键值对。然后进行数据库的操作。

func (m *FriendModel) Insert(userid uint32, friendid uint32) (bool, error) {
    // 组装 SQL 语句
    if !m.lock("FriendInsert") {
        return false,fmt.Errorf("failed to acquire lock")
    }
    defer func() {
        m.Pool.Get().Do("DEL", "lock:FriendInsert")
    }()
  // 组装 SQL 语句
    sql := fmt.Sprintf("INSERT INTO friend VALUES('%d', '%d')", userid, friendid)
    // 执行 SQL 语句
    _, err := m.Db.Exec(sql)
    
    if err != nil {
        return false, err
    }

    return true, nil
}

三、redis实现分布式锁出现的经典问题

死锁问题问题

如果获取锁的进程在持有锁期间出现了宕机等异常情况,那么可能会导致锁一直不能被释放,这样会导致其他进程无法获取到锁,相当于锁失效了。为了避免这种情况的发生,需要对锁设置超时时间,一旦超时时间到了,锁会自动释放。加锁逻辑与设置过期时间要是原子操作,一起操作的。

锁不住与删除别人锁问题

A线程获得锁之后卡了30秒,导致已释放锁了,导致锁不住,B线程拿到锁之后,原来的A线程接着执行,删除掉了线程B的锁,导致删除别人锁问题

 **删除别人的锁问题解决:**可以将锁的 value 设置为一个唯一的标识,例如是一个包含了 UUID 的字符串,在客户端删除锁时,首先需要检查该客户端所持有的锁是否与 Redis 中的锁一致,即锁的 value 值是否与客户端持有的一样,以确保只有占用锁的客户端可以删除其所持有的锁。     
**进一步解决问题:**如果A卡在了删除 锁的前一行,也几句是说已经判断过了取出的value是属于自己的,也就是说拿value,比对value和解锁并没有保证原子性,使用lua表达式保证原子性解决。就是传一个控制字符串给redis.

锁不住问题解决(锁过期了,业务没执行完,需要续期):

Redisson - 是一个高级的分布式协调Redis客户,redisson很好的解决了redis在分布式环境下的一些棘手问题
它的宗旨就是让使用者减少对Redis的关注,将更多精力用在处理业务逻辑上。redisson对分布式锁做了很好封装,只需调用API即可。RLock lock = redissonClient.getLock("stockLock");
 redisson在加锁成功后,会注册一个定时任务监听这个锁,每隔10秒就去查看这个锁,如果还持有锁,就对过期时间进行续期。默认过期时间30秒。这个机制也被叫做:“看门狗”
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Redis分布式锁实现原理是基于Redis的SETNX指令和过期时间实现的。具体实现步骤是:当某个客户端请求获取锁时,该客户端向Redis服务器发送SETNX命令,若SETNX命令返回值为1,则表示获取锁成功,该客户端接着设置一个过期时间,用以避免锁一直持有;若SETNX命令返回值为0,则表示获取锁失败,此时客户端需要等待一段时间后重新尝试获取锁。 ### 回答2: Redis分布式锁是一种在分布式环境下协调多个进程或线程之间的互斥访问资源的机制。其原理是利用Redis提供的原子操作,通过在Redis中设置一个特定的键值对来实现锁的获取和释放。 具体实现步骤如下: 1. 获取锁:客户端通过执行SETNX命令(SET if Not eXists)来尝试在Redis中设置一个指定的键,并为其设置一个过期时间。如果命令成功执行并返回1,表示获取到了锁,否则表示锁已被其他客户端占用。 2. 使用锁:获取到锁之后,执行需要互斥访问资源的操作。 3. 释放锁:操作完成后,客户端通过执行DEL命令来删除所创建的键,释放锁。 需要注意的是,为了防止锁过期时间过长导致锁永远不会释放,可以使用SET命令来给锁设置一个过期时间,保证即使发生异常情况,锁也会在一段时间后自动释放。 此外,还需要考虑到锁的重入性和死锁的情况。对于锁的重入性,可以在Redis中维护一个计数器,记录获取锁的次数,在释放锁时将计数器减1,直到计数器为0时才真正释放锁。对于死锁的情况,可以为锁设置一个超时时间,如果获取锁的客户端在规定的时间内没有释放锁,则认为发生了死锁,其他客户端可以尝试获取该锁。 总之,Redis分布式锁通过利用Redis的原子操作和过期时间机制,可以实现多个进程或线程之间的资源互斥访问,确保系统在分布式环境下的稳定运行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值