用 Go + Redis 实现分布式锁
为什么需要分布式锁
- 用户下单
锁住 uid,防止重复下单。
- 库存扣减
锁住库存,防止超卖。
- 余额扣减
锁住账户,防止并发操作。 分布式系统中共享同一个资源时往往需要分布式锁来保证变更资源一致性。
分布式锁需要具备特性
- 排他性
锁的基本特性,并且只能被第一个持有者持有。
- 防死锁
高并发场景下临界资源一旦发生死锁非常难以排查,通常可以通过设置超时时间到期自动释放锁来规避。
- 可重入
锁持有者支持可重入,防止锁持有者再次重入时锁被超时释放。
- 高性能高可用
锁是代码运行的关键前置节点,一旦不可用则业务直接就报故障了。高并发场景下,高性能高可用是基本要求。
实现 Redis 锁应先掌握哪些知识点
- set 命令
> SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX
second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。PX
millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。NX
:只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。XX
:只在键已经存在时,才对键进行设置操作。
- Redis.lua 脚本
使用 redis lua 脚本能将一系列命令操作封装成 pipline 实现整体操作的原子性。
go-zero 分布式锁 RedisLock 源码分析
core/stores/redis/redislock.go
- 加锁流程
-- 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
- 解锁流程
-- 释放锁 -- 不可以释放别人的锁 if redis.call("GET", KEYS[1]) == ARGV[1] then -- 执行成功返回“1” return redis.call("DEL", KEYS[1]) else return 0 end
- 源码解析
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) }
关于分布式锁还有哪些实现方案
项目地址
https://github.com/zeromicro/go-zero
https://gitee.com/kevwan/go-zero
欢迎使用 go-zero
并 star 支持我们!
微信交流群
关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Mozilla 公布最新财报,净资产持续增长
WOT全球技术创新大会2022,门票6折抢购中!购票立减2320元! Mozilla 基金会近日公布了其 2020(2019 年 12 月 31 日—— 2020 年 12 月 31 日)年的财务报告,根据财报数据显示,Mozilla 在 2020 年的收入为 4.96 亿美元。 Mozilla 基金会在去年的营收为 8.28 亿美元,如果这么直接对比的话,Mozilla 在 2020 年的收入相比去年下滑了约 40%。但需要留意的是,去年 Moziila 的收入中有 3.38 亿美元来自与前搜索引擎提供商雅虎的法律纠纷。 因此,如果把这一部分收入排除在外的话,Mozilla 在 2020 年的收入相比去年增加了约 600 万美元。 收入 单看 Mozilla 的各项收入来源,来自专利权使用费(royalties)的收入仍然是 Mozilla 最主要的收入来源。但这方面的收入从大约 4.51 亿美元下降到 4.41 亿美元,其中包括 Mozilla 与搜索引擎提供商所签订协议的这部分收入。 Mozilla 过去一年在订阅和广告方面的收入(subscription and adverti...
- 下一篇
Bean Searcher 发布 v3.0.5 版本
Bean Searcher 发布 v3.0.5 版本,,具体更新内容如下: Bean Searcher 无@DbIgnore也自动忽略实体类中的静态字段 Bean Searcher Boot Starter 使用 Searcher 类型注入检索器时,默认注入 MapSearcher,不再报错 提高兼容性,SpringBoot 最低版本支持到 v1.4+ https://github.com/ejlchina/bean-searcher https://gitee.com/ejlchina-zhxu/bean-searcher 2021年度 OSC 中国最佳开源项目评选,来投上一票吧: https://www.oschina.net/project/top_cn_2021/?id=627
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS关闭SELinux安全模块
- Linux系统CentOS6、CentOS7手动修改IP地址
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7设置SWAP分区,小内存服务器的救世主