美文网首页
go-zero基础组件-分布式锁RedisLock

go-zero基础组件-分布式锁RedisLock

作者: ouyangan | 来源:发表于2021-11-11 10:40 被阅读0次

为什么需要分布式锁

  1. 用户下单

锁住uid,防止重复下单。

  1. 库存扣减

锁住库存,防止超卖。

  1. 余额扣减

锁住账户,防止并发操作。
分布式系统中共享同一个资源时往往需要分布式锁来保证变更资源一致性。

分布式锁需要具备特性

  1. 排他性

锁的基本特性,并且只能被第一个持有者持有。

  1. 防死锁

高并发场景下临界资源一旦发生死锁非常难以排查,通常可以通过设置超时时间到期自动释放锁来规避。

  1. 可重入

锁持有者支持可重入,防止锁持有者再次重入时锁被超时释放。

  1. 高性能高可用

锁是代码运行的关键前置节点,一旦不可用则业务直接就报故障了。高并发场景下,高性能高可用是基本要求。

实现Redis锁应先掌握哪些知识点

  1. set命令

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EXsecond :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PXmillisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX:只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX:只在键已经存在时,才对键进行设置操作。
  1. Redis.lua脚本

使用redis lua脚本能将一系列命令操作封装成pipline实现整体操作的原子性。

go-zero分布式锁RedisLock源码分析

core/stores/redis/redislock.go

  1. 加锁流程
--KEYS[1]: 锁key
--ARGV[1]: 锁value,随机字符串
--ARGV[2]: 过期时间
--判断锁key持有的value是否等于传入的value
--如果相等说明是再次获取锁并更新获取时间,防止重入时过期
--这里说明是“可重入锁”
if redis.call("GET", KEYS[1]) == ARGV[1] then
    --设置
    redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
    return "OK"

else
    --锁key.value不等于传入的value则说明是第一次获取锁
    --SET key value NX PX timeout : 当key不存在时才设置key的值
    --设置成功会自动返回“OK”,设置失败返回“NULL Bulk Reply”
    --为什么这里要加“NX”呢,因为需要防止把别人的锁给覆盖了
    return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end
image
  1. 解锁流程
--释放锁
--不可以释放别人的锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
    --执行成功返回“1”
    return redis.call("DEL", KEYS[1])
else
    return 0
end
image
  1. 源码解析
package redis

import (
    "math/rand"
    "strconv"
    "sync/atomic"
    "time"

    red "github.com/go-redis/redis"
    "github.com/tal-tech/go-zero/core/logx"
)

const (
    letters     = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
    redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
    return "OK"
else
    return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end`
    delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end`
    randomLen = 16
    //默认超时时间,防止死锁
    tolerance       = 500 // milliseconds
    millisPerSecond = 1000
)

// A RedisLock is a redis lock.
type RedisLock struct {
    //redis客户端
    store *Redis
    //超时时间
    seconds uint32
    //锁key
    key string
    //锁value,防止锁被别人获取到
    id string
}

func init() {
    rand.Seed(time.Now().UnixNano())
}

// NewRedisLock returns a RedisLock.
func NewRedisLock(store *Redis, key string) *RedisLock {
    return &RedisLock{
        store: store,
        key:   key,
        //获取锁时,锁的值通过随机字符串生成
        //实际上go-zero提供更加高效的随机字符串生成方式
        //见core/stringx/random.go:Randn
        id:    randomStr(randomLen),
    }
}

// Acquire acquires the lock.
//加锁
func (rl *RedisLock) Acquire() (bool, error) {
    //获取过期时间
    seconds := atomic.LoadUint32(&rl.seconds)
    //默认锁过期时间为500ms,防止死锁
    resp, err := rl.store.Eval(lockCommand, []string{rl.key}, []string{
        rl.id, strconv.Itoa(int(seconds)*millisPerSecond + tolerance),
    })
    if err == red.Nil {
        return false, nil
    } else if err != nil {
        logx.Errorf("Error on acquiring lock for %s, %s", rl.key, err.Error())
        return false, err
    } else if resp == nil {
        return false, nil
    }

    reply, ok := resp.(string)
    if ok && reply == "OK" {
        return true, nil
    }

    logx.Errorf("Unknown reply when acquiring lock for %s: %v", rl.key, resp)
    return false, nil
}

// Release releases the lock.
//释放锁
func (rl *RedisLock) Release() (bool, error) {
    resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})
    if err != nil {
        return false, err
    }

    reply, ok := resp.(int64)
    if !ok {
        return false, nil
    }

    return reply == 1, nil
}

// SetExpire sets the expire.
//需要注意的是需要在Acquire()之前调用
//不然默认为500ms自动释放
func (rl *RedisLock) SetExpire(seconds int) {
    atomic.StoreUint32(&rl.seconds, uint32(seconds))
}

func randomStr(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

关于分布式锁还有哪些实现方案

  1. etcd
  2. redis redlock

相关文章

  • go-zero基础组件-分布式锁RedisLock

    为什么需要分布式锁 用户下单 锁住uid,防止重复下单。 库存扣减 锁住库存,防止超卖。 余额扣减 锁住账户,防止...

  • Redis分布式锁

    RedisLock 自己实现一个Redis分布式锁 SpringContextHolder

  • Redis实现分布式锁

    1. 分布式锁分类 数据库乐观锁 基于Redis的分布式锁 基于ZooKeeper的分布式锁 2. 组件依赖 po...

  • go-zero基础组件-分布式限流periodLimit

    如今微服务架构大行其道本质原因是因为要降低系统的整体复杂度,将系统风险均摊到子系统从来最大化保证系统的稳定性,通过...

  • go-zero基础组件-分布式限流tokenLimit

    上篇文章提到固定时间窗口限流无法处理突然请求洪峰情况,本文讲述的令牌桶线路算法则可以比较好的处理此场景。 工作原理...

  • 秒杀开发架构

    做法一:使用加锁操作先占有锁资源,再占有红包。 可以使用分布式全局锁的方式(各种分布式锁组件或者数据库锁),先申请...

  • 【学习】Spring微服务

    分布式理论 分布式基础理论微服务基础理论分布式事务分布式一致性分布式缓存分布式锁分布式Session负载均衡 Sp...

  • 知识整理总结

    一、Java基础篇1、锁synchronized和ReentrantLock区别,编译后区别,分布式锁(zk、re...

  • 【分布式锁第三篇 基于Redis的分布式锁】

    基于单Redis节点的分布式锁 组件依赖 首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入...

  • 分布式锁

    为什么要用分布式锁 数据库乐观锁redis分布式锁zookeeper分布式锁 使用分布式锁的场景 实现分布式锁的方...

网友评论

      本文标题:go-zero基础组件-分布式锁RedisLock

      本文链接:https://www.haomeiwen.com/subject/hdcizltx.html