本文思路来自Wx公众号:小徐生先生的变成世界,原文地址
个人理解分布式锁是分布式服务器的单机锁,对于单机锁是保证服务器在同一时间只能有一个线程能访问该方法。但是对于分布式服务器来说,可能存在多台服务器接收用户请求,这样请求在不同服务器的数据就没办法通过单机锁来阻塞。所以才需要通过额外的组件,实现多服务器之间的管理。
回顾 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)
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...)
}
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
}
}
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()
}