1 数据结构
type ExpireLock struct {
// 核心锁
mutex sync.Mutex
// 流程锁,防止多次解锁,例如异步解锁协程解锁和手动解锁同时发生
processMutex sync.Mutex
// 锁的身份标识
token string
// 停止异步解锁协程函数
stop context.CancelFunc
}
2 总体流程
context原理可以看go的context原理_lunar@qq.com的博客-CSDN博客
Lock()方法流程如下:
- 加核心锁mutex.Lock()
- 设置锁的身份标识token -- 标识这把锁的主人是谁
- 检查过期时间,如果小于等于0,就表示手动释放锁,不需开启异步超时解锁协程,大于0就开启一个异步超时解锁协程,过期自动解锁(这里启动的异步协程主要由父context来控制其生命周期)
-
给终止异步协程函数stop赋值,启动异步协程,达到指定时间后执行解锁操作
Unlock()方法流程如下:
- 先加流程锁processMutex.Lock(),防止并发情况下,Lock方法开启的异步超时解锁协程过期时间到了开始执行释放锁操作和手动解锁同时发生
- 校验token
- 调用stop函数,停止Lock方法开启的异步超时解锁协程
- 重置token
- 解锁
3 源码
package redis_lock_test
import (
"context"
"errors"
"redis-lock-test/utils"
"sync"
"time"
)
type ExpireLock struct {
// 核心锁
mutex sync.Mutex
// 流程锁,防止多次解锁,例如异步解锁协程解锁和手动解锁同时发生
processMutex sync.Mutex
// 锁的身份标识
token string
// 停止异步解锁协程函数
stop context.CancelFunc
}
func NewExpireLock() *ExpireLock {
return &ExpireLock{}
}
// Lock 加锁
func (e *ExpireLock) Lock(expireTime int64) {
// 1. 加锁
e.mutex.Lock()
// 2,设置锁的身份标识token
token := utils.GetProcessAndGoroutineIDStr()
e.token = token
// 2.1 校验一下过期时间,如果小于等于0,就代表手动释放锁,无需开启异步解锁协程
if expireTime <= 0 {
return
}
// 3.给终止异步协程函数stop赋值,启动异步协程,达到指定时间后执行解锁操作
ctx, cancel := context.WithCancel(context.Background())
e.stop = cancel
go func() {
select {
// 到了锁的过期时间,释放锁
case <-time.After(time.Duration(expireTime) * time.Second):
e.unlock(token)
case <-ctx.Done():
}
}()
}
// Unlock 解锁
func (e *ExpireLock) Unlock() error {
return e.unlock(utils.GetProcessAndGoroutineIDStr())
}
func (e *ExpireLock) unlock(token string) error {
// 1.加流程锁,防止并发情况下,异步解锁协程解锁和手动解锁同时发生
e.processMutex.Lock()
defer e.processMutex.Unlock()
// 2.校验token
if e.token != token {
return errors.New("unlock not your lock")
}
// 3.停止异步解锁协程
if e.stop != nil {
e.stop()
}
// 4.重置token
e.token = ""
// 5.解锁
e.mutex.Unlock()
return nil
}
// utils包
package utils
import (
"fmt"
"os"
"runtime"
"strconv"
"strings"
)
func GetCurrentProcessID() string {
return strconv.Itoa(os.Getpid())
}
// GetCurrentGoroutineID 获取当前的协程ID
func GetCurrentGoroutineID() string {
buf := make([]byte, 128)
buf = buf[:runtime.Stack(buf, false)]
stackInfo := string(buf)
return strings.TrimSpace(strings.Split(strings.Split(stackInfo, "[running]")[0], "goroutine")[1])
}
func GetProcessAndGoroutineIDStr() string {
return fmt.Sprintf("%s_%s", GetCurrentProcessID(), GetCurrentGoroutineID())
}