分布式锁的原理和实现(Go)

思维导图

在这里插入图片描述

为什么需要分布式锁?

保证分布式系统并发请求或不同服务实例操作共享资源的安全性,确保在同一时间内,仅有一个进程能够修改共享资源,例如数据库记录或文件,主要用于解决分布式环境中的数据一致性和并发控制问题。 应用场景:用户下单,库存扣减,余额扣减。我们的场景:防止用户信息多写,积分扣减(扣减时几个请求都满足积分余额可扣的情况,只允许一个去扣),防止重复设置定时任务。
预扣费用时用分布式锁是为了确保在扣减权益余额时的并发安全性。在处理多个请求同时尝试冻结同一个用户的某个特定权益时,分布式锁可以确保只有一个请求能够执行扣减余额的操作,从而避免出现并发问题,如扣减多次、余额不足等错误情况。

  • 使用分布式锁可能会对性能产生一定的影响,但这是为了确保数据的一致性和正确性所必需的;如果操作是操作是幂等的(即使多次执行也会产生相同的结果),可能不需要分布式锁;
  • 如果共享资源是MySQL数据,可以用MySQL 的乐观锁 SELECT FOR UPDATE 来实现分布式锁的。
    在这里插入图片描述

go语言分布式锁的实现

Redis

https://github.com/zeromicro/go-zero/blob/master/core/stores/redis/redislock.go
go-zero里已经实现了redislock,但没有续约机制

自己的实现

  • 为什么需要自己封装? 用的框架里没有,去参考了go-zero,里面的分布式锁有基础的加锁和解锁,缺少续约
// 需要实现的能力
// 1.排他性、原子性
// 2.主动释放/自动释放
// 3.可重入
// 4.可续约

package r_lc

import (
	"context"
	"errors"
	"github.com/go-redis/redis/v8"
	"time"

	"github.com/google/uuid"
)

// 实现以下4个方法
type Lock interface {
   
	// TryLock 尝试锁
	TryLock(ctx context.Context) (lcNum int, res bool)
	// LockWait 尝试锁并等待
	LockWait(ctx context.Context, wait time.Duration) (lcNum int, res bool)
	// Renew 续约
	Renew(ctx context.Context)
	// Unlock 解锁
	Unlock(ctx context.Context) (leftLcNum int, err error)
}

// RLc 基于redis的分布式锁
type RLc struct {
   
	rdb *redis.Client
	// key 锁标识
	key string

	// lcTag 唯一标识,防止串锁
	lcTag string

	// expiresIn 过期时间
	expiresIn time.Duration

	// releaseCh 锁释放信号 (看门狗)
	releaseCh chan struct{
   }

	// RetryInterval LockWait重试锁的间隔。默认100ms
	RetryInterval time.Duration

	// RenewInterval 续约锁间隔,默认为expiresIn/2
	RenewInterval time.Duration

	// MaxRenewDur 自动续约最长时间。默认1小时,当expiresIn大于1小时,为expiresIn
	MaxRenewDur time.Duration
}

type RlcOpt func(lc *RLc)

const (
	retryIntervalDefault = 100 * time.Millisecond
	maxRenewDurDefault   = time.Hour
)

// LUA脚本
var (
	// tryLockLua
	// return 0. 加锁失败
	// return >0. 加锁成功,当前锁的数量
	tryLockLua = `
local key = KEYS[1]
local val = ARGV[1]
local expiresIn = ARGV[2]

-- 锁不存在,加锁
if redis.call('EXISTS', key) == 0 then
	redis.call('HINCRBY', key, val, 1)
	redis.call('PEXPIRE', key, expiresIn)
	return 1
end

-- 锁存在,判断持有锁,增加加锁次数 (可重入)
if redis.call('HEXISTS', key, val) == 1 then
    return redis.call('HINCRBY', key, val, 1)
end

-- 锁被其他进程占用
return 0

`

	// unlockLua
	// return > 0. 剩余待解锁次数
	// return = 0. 解锁成功
	// return = -1. 锁不存在 | 未持有锁
	unlockLua = `
local key = KEYS[1]
local val = ARGV[1]

-- 锁不存在或未持有锁
if redis.call('HEXISTS', key, val) == 0 then
	return -1
end

-- 按次数解锁
local count = redis.call('HINCRBY', key, val, -1)
if count <= 0 then
	-- 全部解锁
	redis.call("DEL",key)
	return 0
end

-- 剩余待解锁次数
return count
`
	// renewLua
	// return 0. 续约失败
	// return 1. 续约成功
	renewLua = `
local key = KEYS[1]
local val = ARGV[1]
local expiresIn = ARGV[2]

-- 锁不存在或未持有锁
if redis.call('HEXISTS', key, val) == 0 then
	return 0
end

-- 设置过期时间
return redis.call('PEXPIRE', key, expiresIn)
`
)

var (
	ErrLostKey = errors.New("lost key") // 锁不存在或被其他进程占用
)

func NewRLc(rdb *redis.Client, key string, expiresIn time.Duration, opts ...RlcOpt) *RLc {
   
	lc := &RLc{
   
		rdb:       rdb,
		key:       key,
		lcTag:     uuid.New().String(),
		expiresIn: expiresIn,
		releaseCh: make(chan struct{
   }),
	}
	for _, opt := range opts {
   
		opt(lc)
	}
	if lc.RetryInterval == 0 {
   
		lc.RetryInterval = retryIntervalDefault
	}
	if lc.RenewInterval == 0 {
   
		lc.RenewInterval 
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值