分布式锁

个人理解分布式锁是分布式服务器的单机锁,对于单机锁是保证服务器在同一时间只能有一个线程能访问该方法。但是对于分布式服务器来说,可能存在多台服务器接收用户请求,这样请求在不同服务器的数据就没办法通过单机锁来阻塞。所以才需要通过额外的组件,实现多服务器之间的管理。

选型Redis的原因

  1. Redis高效且具备高可用性,当提供分布式锁服务的基础组件中存在少量节点发生故障时,不应该影响到分布式锁服务的稳定性。
  2. Redis是kv的形式存储信息,可以保证存储锁的同时,存储对应的用户信息。
  3. 健壮性:对于数据Redis可以设置过期时间,杜绝了死锁的产生,在很久没有操作的时候会自动释放锁。
  4. 独占性,因为key是存在唯一性的,所以不会存在同一时间有两个锁的情况。
  5. SETNX语句,Redis存在SETNX,支持创建前先查询是否存在相同的key,操作更加方便。
  6. 存在mysql相似的事务功能Eval,可以将多个数据合并操作,保证了解锁时的原子性。

实现思路

  • 针对于同一把分布式锁,使用同一条数据进行标识(以 redis 为例,则为同一个 key 对应的 kv 数据记录)
  • 假如在存储介质成功插入了该条数据(要求之前该 key 对应的数据不存在),则被认定为加锁成功
  • 把从存储介质中删除该条数据这一行为理解为释放锁操作
  • 倘若在插入该条数据时,发现数据已经存在(锁已被他人持有),则持续轮询,直到数据被他人删除(他人释放锁),并由自身完成数据插入动作为止(取锁成功)
  • 由于是并发场景,需要保证
  • 检查数据是否已被插入
  • 数据不存在则插入数据 】这两个步骤之间是原子化不可拆分的(在 redis 中是 set only if not exist —— SETNX 操作)

弱一致性问题

回顾 redis 的设计思路,为避免单点故障问题,redis 会基于主从复制的方式实现数据备份. (以哨兵机制为例,哨兵会持续监听 master 节点的健康状况,倘若 master 节点发生故障,哨兵会负责扶持 slave 节点上位,以保证整个集群能够正常对外提供服务). 此外,在 CAP 体系中,redis 走的是 AP 路线,为保证服务的吞吐性能,主从节点之间的数据同步是异步延迟进行的.
到这里问题就来了,试想一种场景:倘若 使用方 A 在 redis master 节点加锁成功,但是对应的 kv 记录在同步到 slave 之前,master 节点就宕机了. 此时未同步到这项数据的 slave 节点升为 master,这样分布式锁被 A 持有的“凭证” 就这样凭空消失了. 于是不知情的使用方 B C D 都可能加锁成功,于是就出现了一把锁被多方同时持有的问题,导致分布式锁最基本的独占性遭到破坏.
关于这个问题,一个比较经典的解决方案是:redis 红锁(redlock,全称 redis distribution lock)

sdk介绍跟项目开源地址

  • 首先,本文使用到基于 golang 编写的 redis 客户端 sdk:redigo,用于和 redis 组件进行交互redigo 开源地址
  • 小徐老师的分布式程序开源地址,其中还提供了引用sdk,如果不想了解项目代码的,可以直接go mod 引用sdk就可以使用啦

源码部分,其中包含自己的一些见解

封装Client客户端,以下介绍一下方法的具体作用

  1. Client类
  1. 继承配置文件ClientOptions
  1. MaxIdel 最大空闲连接
  2. idelTimeoutSeconds 超时时间
  3. max Active 最大活跃连接
  1. pool redis连接池
  1. NewClient 创建客户端方法
  2. getRedisPool 获取Redis连接池
  3. getRedisConn 生成redis操作实例,会存到pool中
  4. getConn 根据上下文context到pool中获取操作实例
  5. setNEX 封装过期时间,key判断跟添加操作到一起
  6. Eval 支持事务形式的操作,为了后续解锁做准备
package redisLock

import (
	"context"
	"errors"
	"fmt"
	"github.com/gomodule/redigo/redis"
	"time"
)

type Client struct {
	// 继承 ClientOptions
	ClientOptions
	// pool 连接池,用来存储redis连接的
	pool *redis.Pool
}

func (this *Client) Println() {
	fmt.Println(this.network, this.address, this.maxIdle)
}

func NewClient(network string, address string, password string, opts ...ClientOption) *Client {

	fmt.Println(2)
	// 创建Client,分配基础配置
	c := Client{
		ClientOptions: ClientOptions{
			network:  network,
			address:  address,
			password: password,
		},
	}

	// 传参为函数的时候,就已经执行了函数方法了
	// 执行顺序是 传参时执行 函数的方法体 --> 执行完执行接收传参的方法 --> 执行到传第二次参才进行第二次return --> 执行return方法体
	// opt = WithMaxIdle(4).return
	for _, opt := range opts {
		fmt.Println(3)
		// opt() == func(c *ClientOptions)
		// opt 即 return方法
		// 如果此刻不传参,则不执行return方法
		opt(&c.ClientOptions)
	}

	// 默认初始化
	repairClient(&c.ClientOptions)

	// 创建线程池
	pool := c.getRedisPool()

	fmt.Println("创建客户端成功...")

	// 线程池分配给新的client
	return &Client{
		pool: pool,
	}

}

// getRedisPool 创建RedisPool连接池
func (this *Client) getRedisPool() *redis.Pool {
	return &redis.Pool{
		// 最大空闲
		MaxIdle: this.maxIdle,
		// 最大连接
		MaxActive: this.maxActive,
		// 超时时间
		IdleTimeout: time.Duration(this.idleTimeoutSeconds) * time.Second,
		// 客户端存储的地方? redis.Conn
		Dial: func() (redis.Conn, error) {
			c, err := this.getRedisConn()
			if err != nil {
				return nil, err
			}
			return c, nil
		},
		// 等待阻塞
		Wait: this.wait,
		// 测试连接方式?
		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			_, err := c.Do("PING")
			return err
		},
	}
}

// getRedisConn 生成RedisConn连接
func (c *Client) getRedisConn() (redis.Conn, error) {
	// 判断address是否为空
	if c.address == "" {
		panic("Cannot get redis address from config")
	}
	// 连接配置文件?
	var dialOpts []redis.DialOption
	if len(c.password) > 0 {
		// 密码不为空,则设置密码进入配置文件
		dialOpts = append(dialOpts, redis.DialPassword(c.password))
	}
	// 使用background生成上下文
	conn, err := redis.DialContext(context.Background(),
		c.network, c.address, dialOpts...)
	if err != nil {
		return nil, err
	}
	return conn, nil
}

// GetConn 从连接池获取conn
func (c *Client) GetConn(ctx context.Context) (redis.Conn, error) {
	// 连接池自带方法 GetContext,传入context
	return c.pool.GetContext(ctx)
}

// SetNEX 加锁操作,包含过期时间等参数设置
func (c *Client) SetNEX(ctx context.Context, key, value string, expireSeconds int64) (string, error) {

	// 判断键值是否为空,空的话不需要获取conn,减少空间消耗
	if key == "" || value == "" {
		return "", errors.New("key or value is null!")
	}
	// 根据上下文获取conn
	conn, err := c.GetConn(ctx)
	if err != nil {
		return "", err
	}
	defer conn.Close()

	// 拼接redis语句,并发送给redis
	reply, err := conn.Do("SET", key, value, "EX", expireSeconds, "NX")
	// 判断接收数据,是否报错
	if err != nil {
		return "", err
	}
	if reply == nil {
		return "", nil
	}
	// 将接收的更改行转化为int64的状态
	return reply.(string), nil
}

// Eval 支持redis的事务操作,为了解锁操作统一而确定的
func (c *Client) Eval(ctx context.Context, src string, keyCount int, keysAndArgs []interface{}) (interface{}, error) {
	// ctx 上下文参数,用来获取conn
	args := make([]interface{}, 2+len(keysAndArgs))
	args[0] = src
	// 这是干嘛的????key有几个的标志?
	args[1] = keyCount
	copy(args[2:], keysAndArgs)

	conn, err := c.pool.GetContext(ctx)
	if err != nil {
		return -1, err
	}
	defer conn.Close()
	return conn.Do("EVAL", args...)
}

Options 配置文件(客户端 + 锁)

  1. ClientOptions 客户端配置类,具体内容在上面介绍过了
    2. wait 阻塞判断值
    3. network 网络连接方式
    4. address 连接地址
    5. password redis密码
  2. ClientOption 高阶函数返回值,用方法来作为返回值,很有趣的设计
  3. WithMaxIdel 设置参数
  4. WithIdelTimeoutSeconds 设置参数
  5. WithMaxActive 设置参数
  6. WithWaitMode 设置参数
  7. repairClient 默认设置
  8. LockOptions 锁的配置类
  1. isBlock 阻塞判断
  2. blockWaitingSeconds 阻塞等待时间
  3. expireSeconds 锁过期时间
  1. WithBlock 设置参数
  2. WithBlockWaitingSeconds 设置参数
  3. WithExpireSeconds 设置参数
package redisLock

import "fmt"

const (
	// DefaultIdleTimeoutSeconds 默认连接池超过 10 s 释放连接
	DefaultIdleTimeoutSeconds = 10
	// DefaultMaxActive 默认最大激活连接数
	DefaultMaxActive = 100
	// DefaultMaxIdle 默认最大空闲连接数
	DefaultMaxIdle = 20
)

type ClientOptions struct {
	maxIdle            int
	idleTimeoutSeconds int
	maxActive          int
	wait               bool
	// 必填项目
	network  string
	address  string
	password string
}

// ClientOption 作为高阶函数的返回值,可以根据方法的不同进行多种操作
type ClientOption func(*ClientOptions)

// WithMaxIdle 设置最大空闲数量
func WithMaxIdle(maxIdle int) ClientOption {
	fmt.Println("1")
	return func(c *ClientOptions) {
		fmt.Println(4)
		c.maxIdle = maxIdle
	}
}

// WithIdleTimeoutSeconds 设置超时时间
func WithIdleTimeoutSeconds(idleTimeoutSeconds int) ClientOption {
	return func(c *ClientOptions) {
		c.idleTimeoutSeconds = idleTimeoutSeconds
	}
}

// WithMaxActive 设置最大激活链接
func WithMaxActive(maxActive int) ClientOption {
	return func(c *ClientOptions) {
		c.maxActive = maxActive
	}
}

// WithWaitMode 等待模型?
func WithWaitMode() ClientOption {
	return func(c *ClientOptions) {
		c.wait = true
	}
}

// repairClient Client 默认设置
func repairClient(c *ClientOptions) {
	if c.maxIdle < 0 {
		c.maxIdle = DefaultMaxIdle
	}

	if c.idleTimeoutSeconds < 0 {
		c.idleTimeoutSeconds = DefaultIdleTimeoutSeconds
	}

	if c.maxActive < 0 {
		c.maxActive = DefaultMaxActive
	}
}

// ---------------------------------------------------------------------------------------------------------------------

// LockOptions 锁配置文件
type LockOptions struct {
	// 是否阻塞
	isBlock bool
	// 阻塞等待时间
	blockWaitingSeconds int64
	// 锁过期时间
	expireSeconds int64
}

// LockOption 高阶函数返回值
type LockOption func(options *LockOptions)

// WithBlock 设置阻塞
func WithBlock() LockOption {
	return func(o *LockOptions) {
		fmt.Println("设置阻塞成功...")
		o.isBlock = true
	}
}

// WithBlockWaitingSeconds 设置阻塞等待时间
func WithBlockWaitingSeconds(blockWaitingSeconds int64) LockOption {
	return func(options *LockOptions) {
		options.blockWaitingSeconds = blockWaitingSeconds
	}
}

// WithExpireSeconds 设置锁的过期时间
func WithExpireSeconds(expireSeconds int64) LockOption {
	return func(options *LockOptions) {
		options.expireSeconds = expireSeconds
	}
}

// repairLock Lock默认设置
func repairLock(options *LockOptions) {
	if options.isBlock && options.blockWaitingSeconds <= 0 {
		// 等待时间为5s
		options.blockWaitingSeconds = 5
	}
	if options.expireSeconds <= 0 {
		// 过期时间为30s
		options.expireSeconds = 30
	}
}

redisLock 分布式锁具体实现文件

  1. ErrLockAcquiredByOthers 锁获取失败的返回值
  2. IsRetryableErr 判断是否继续获取
  3. RedisLock
  1. 继承LockOptions
  2. key 锁名
  3. token 取锁人
  4. client 客户端,用于获取线程池
  1. NewRedisLock 创建锁
  2. Lcok 加锁
  3. tryLock 获取锁
  4. getLockKey 返回锁名
  5. blockingLick 轮询锁
  6. UnLock 解锁
package redisLock

import (
	"context"
	"errors"
	"fmt"
	"time"
)

const RedisLockKeyPrefix = "REDIS_LOCK_PREFIX_"

// LuaCheckAndDeleteDistributionLock 判断是否拥有分布式锁的归属权,是则删除
const LuaCheckAndDeleteDistributionLock = `
  local lockerKey = KEYS[1]
  local targetToken = ARGV[1]
  local getToken = redis.call('get',lockerKey)
  if (not getToken or getToken ~= targetToken) then
    return 0
  else
    return redis.call('del',lockerKey)
  end
`

// ErrLockAcquiredByOthers no use lock error
var ErrLockAcquiredByOthers = errors.New("lock is acquired by others")

// IsRetryableErr is return err and no use lock error
func IsRetryableErr(err error) bool {
	return errors.Is(err, ErrLockAcquiredByOthers)
}

// RedisLock is Lock Type
type RedisLock struct {
	LockOptions
	// LockName
	key string
	// useLockUser
	token string
	// use RedisClient
	client *Client
}

// NewRedisLock 创建锁
func NewRedisLock(key string, client *Client, opts ...LockOption) *RedisLock {
	r := RedisLock{
		key:    key,
		client: client,
		// 工具生成token
		token: GetProcessAndGoroutineIDStr(),
	}

	for _, opt := range opts {
		opt(&r.LockOptions)
	}
	// 默认参数设置
	repairLock(&r.LockOptions)

	fmt.Println("创建锁成功...", r.GetToken())

	return &r
}

// Lock 加锁
func (r *RedisLock) Lock(ctx context.Context) error {
	// 尝试获取锁
	err := r.tryLock(ctx)
	// 不存在锁,加锁成功,直接返回
	if err == nil {
		return nil
	}
	// 加锁失败进行下面判断

	// 非阻塞模式加锁失败直接返回错误
	if !r.isBlock {
		return err
	}

	// 判断错误是否可以允许重试,不可允许的类型则直接返回错误
	if !IsRetryableErr(err) {
		return err
	}

	// 基于阻塞模式持续轮询取锁
	return r.blockingLock(ctx)
}

func (r *RedisLock) GetToken() string {
	return r.token
}

// 获取锁
func (r *RedisLock) tryLock(ctx context.Context) error {
	// 首先查询锁是否属于自己
	reply, err := r.client.SetNEX(ctx, r.getLockKey(), r.token, r.expireSeconds)
	// 返回失败则获取失败
	if err != nil {
		return err
	}
	// 返回不等于1,则表示存在锁
	if reply != "OK" {
		return fmt.Errorf("reply: %d, err: %w", reply, ErrLockAcquiredByOthers)
	}
	if reply == "OK" {
		fmt.Println("加锁成功")
	}
	return nil
}

// 返回锁名
func (r *RedisLock) getLockKey() string {
	// 拼接锁名
	return RedisLockKeyPrefix + r.key
}

// 轮询取锁
func (r *RedisLock) blockingLock(ctx context.Context) error {
	// 阻塞模式等锁时间上限,After在等待时间结束后向通道发送当前时间,计时器
	timeoutCh := time.After(time.Duration(r.blockWaitingSeconds) * time.Second)
	// 轮询 ticker,每隔 50 ms 尝试取锁一次,每个时间间隔发送一次时间
	ticker := time.NewTicker(time.Duration(50) * time.Millisecond)
	defer ticker.Stop()
	i := 1
	for range ticker.C {
		select {
		// ctx 终止了
		case <-ctx.Done():
			return fmt.Errorf("lock failed, ctx timeout, err: %w", ctx.Err())
			// 阻塞等锁达到上限时间
		case <-timeoutCh:
			return fmt.Errorf("block waiting time out, err: %w", ErrLockAcquiredByOthers)
		// 拦截上下文过期时间和阻塞上限时间,不拦截轮询,则在轮询发送信息的时候会跳过两个判断进行default操作,尝试取锁
		// 放行
		default:
		}
		i = i + 1
		// 尝试取锁
		fmt.Println("尝试取锁", i)
		err := r.tryLock(ctx)
		if err == nil {
			// 加锁成功,返回结果
			return nil
		}
		fmt.Println("取锁失败")

		// 不可重试类型的错误,直接返回
		if !IsRetryableErr(err) {
			return err
		}
	}

	return nil
}

// Unlock 解锁. 基于 lua 脚本实现操作原子性.
func (r *RedisLock) Unlock(ctx context.Context) error {
	// 获取锁名 + 用户id
	keysAndArgs := []interface{}{r.getLockKey(), r.token}
	/*
	  local lockerKey = KEYS[1]
	  local targetToken = ARGV[1]
	  local getToken = redis.call('get',lockerKey)
	  if (not getToken or getToken ~= targetToken) then
	    return 0
	  else
	    return redis.call('del',lockerKey)
	  end
	*/
	reply, err := r.client.Eval(ctx, LuaCheckAndDeleteDistributionLock, 1, keysAndArgs)
	if err != nil {
		return err
	}

	if ret, _ := reply.(int64); ret != 1 {
		return errors.New("can not unlock without ownership of lock")
	}
	return nil
}

使用方法演示

// main 使用 redisLock
func main() {
	// 创建客户端
	client := redisLock.NewClient("tcp", "127.0.0.1:6379", "")

	// 创建上下文
	ctx := context.Background()

	// 创建锁
	lock1 := redisLock.NewRedisLock("test_key", client, redisLock.WithBlock(),
		redisLock.WithBlockWaitingSeconds(5), redisLock.WithExpireSeconds(5))
	// 创建阻塞锁
	lock2 := redisLock.NewRedisLock("test_key", client, redisLock.WithBlock(),
		redisLock.WithBlockWaitingSeconds(10), redisLock.WithExpireSeconds(1))

	// WaitGroup等待一組go routine完成。主goroutine調用Add來設置要等待的goroutine的数量。
	// 然後每一個goroutine運行並在完成時調用Done。
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		fmt.Println("lock1 开始获取锁")
		if err := lock1.Lock(ctx); err != nil {
			fmt.Println("lock1", err)
			return
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		fmt.Println("lock2 开始获取锁")
		if err := lock2.Lock(ctx); err != nil {
			fmt.Println("lock2", err)
			return
		}
	}()

	wg.Wait()

}