基于 Redis 实现分布式锁

本文介绍了在并发和分布式场景下,如何保证数据一致性。本地锁通过Go的互斥锁实现多线程数据修改,而分布式锁则解决多实例访问共享资源的问题。基于Redis的分布式锁利用SetNX命令加锁,并通过原子脚本解锁,防止并发问题。还讨论了锁的续约机制以应对超时情况。
摘要由CSDN通过智能技术生成

简介

在并发场景下,为了保证数据的一致性,需要通过加锁来实现。单机场景下,多线程修改本地数据,直接通过加锁互斥访问即可实现。但在分布式场景下,如果是多个实例修改一个数据,例如电商中秒杀,可能同时会有多个实例要访问数据库修改商品数量,这时就需要用到分布式锁,让多个实例互斥访问数据库。

本地锁

使用go自带的互斥锁实现多线程数据修改。

package main
import (
	"fmt"
	"sync"
)
type Count struct {
	num int
	mtx sync.Mutex
}

func (c *Count) Change(v int) {
	c.mtx.Lock()
	c.num += v
	c.mtx.Unlock()
}
func main() {
	cnt := Count{num: 0}
	ch := make(chan int, 10)
	N := 1000
	for i := 0; i < N; i++ {
		go func() {
			cnt.Change(1)
			ch <- 1 //线程完成任务后通知主线程
		}()
	}
	for i := 0; i < N; i++ {
		<-ch //每次从chan中拿出一个,拿完N个时,新建的所有线程都执行完毕。
		//注意此时新建线程不一定都结束了,而是线程的内容执行完了
	}
	fmt.Println(cnt)
	/* !+output
	不加锁的情况下:995左右波动(可以自己运行尝试)
	加锁情况下: 1000
	!-output
	*/
}

分布式锁

分布式锁就是在分布式环境下抢占一把锁,是实现分布式系统同步访问共享资源的一种方式。

分布式锁相比本地锁的难点在于需要考虑网络。

基于 Redis 分布式锁需要考虑的问题:

  • 加锁失败/或超时怎么处理?
  • 用户应该只能释放自己加的锁,如何实现
  • 分布式锁的过期时间怎么设置

实现

加锁

基于 Redis 中的 setnx(set if not exists) 来实现加锁,setnx 命令在指定的 key 不存在时,为 key 设置指定的值,设置成功,返回 1 。 设置失败,返回 0 。

使用key作为锁名,同时为了保证只能释放自己加的锁,将val设置为token,只用加锁的用户知道,在释放锁时,如果用户提供的token与Redis中设置的token不同则无法释放锁。

var client *redis.Client

// Lock 实现分布式加锁
// key代表加锁的名称,token是该锁的一个令牌、标识,用来区分是谁加的锁 可以使用 uuid / 生成随机字符串等标识
func Lock(key, token string, expiration time.Duration) (bool, error) {
	ok, err := client.SetNX(key, token, expiration).Result()
	if err != nil {
		ok, err := client.SetNX(key, token, expiration).Result() //如果失败再尝试一次
		return ok, err                                           //无论尝试是否成功,直接返回结果
	}
	return ok, nil
}

释放

获取要释放锁的token,与用户提供的token对比,如果相同释放锁,不同则不释放锁,返回error

// Unlock 实现分布式解锁
// 解锁需要指定锁名和token,通过token可以辨别当前锁是谁持有,不能释放其他人的锁
func Unlock(key, token string) error {
	val, err := client.Get(key).Result() //查询锁key对应的token
	if err != nil {
		return err
	}
	if val != token { //判断是否是该用户加的锁
		return errors.New("解锁失败,不能释放其他人的锁")
	}
	client.Del(key) // 将锁释放
	return nil
}

这里有一个问题,当判断完锁是当前用户持有时(第8行),还没来得及执行释放锁,恰好锁超时过期啦,且其他用户获取了该锁,这时释放锁就会导致释放了他人的锁。

要避免这种情况,可以将查询、判断、释放修改为原子操作。考虑到Redis是单线程服务,使用Redis脚本实现以上三个操作,达到原子效果。

func Unlock1(key, token string) error {
	UnlockScript := redis.NewScript(`
		if redis.call("GET", KEYS[1]) = KEYS[2] then
			return redis.call("del", KEYS[1]) else 
			return 0 
		end`) //新建一个Redis脚本,由于Redis是单线程,所以脚本中的内容一定可以原子执行。

	n, err := UnlockScript.Run(client, []string{key, token}).Result()
	if err != nil {
		return err
	}
	if n == 1 {
		return nil
	}
	return errors.New("解锁失败")
}

续约

通常无法确定一个锁的超时时间,同时也不能设置锁的超时时间为无限长,即没有超时时间,这样当程序异常崩溃时,锁将无法被获取。因此通过续约机制来实现锁的超时时间动态延长。

// 续约机制
func AutoRefresh(key, token string, expiration time.Duration, interval time.Duration) {
	go func() {
		ticker := time.NewTicker(interval)
		for { //一直循环
			select {
			case <-ticker.C:
				val, err := client.Get(key).Result()
				if err != nil || val != token { //判断是否是该用户加的锁
					return //如果不是证明用户的锁已经释放
				}
				_, err = client.Expire(key, expiration).Result() //修改过期时间
				if err != nil {
					return
				}
			}
		}
	}()
}

以上就是基于Redis实现的一个简单分布式锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值