郑州微信网站建设,世界500强企业排名中国名单,网站建站,电子商务网站建设卷子Golang实现Redis分布式锁#xff08;Lua脚本可重入自动续期#xff09;
1 概念
应用场景 Golang自带的Lock锁单机版OK#xff08;存储在程序的内存中#xff09;#xff0c;分布式不行 分布式锁#xff1a; 简单版#xff1a;redis setnx》加锁设置过期时间需要保证原…Golang实现Redis分布式锁Lua脚本可重入自动续期
1 概念
应用场景 Golang自带的Lock锁单机版OK存储在程序的内存中分布式不行 分布式锁 简单版redis setnx》加锁设置过期时间需要保证原子性》lua脚本完整版redis Lua脚本实现可重入自动续期》hset结构 应用场景
防止用户重复下单锁住用户id防止商品超卖问题锁住账户防止并发操作 例如我本地启两个端口跑两个相同服务然后通过Nginx反向代理分别将请求均衡打到两个服务模拟分布式微服务最后通过Jmeter模拟高并发场景。同时我在代码里添加上lock锁。 可以看到还是有消费到相同数据出现超卖现象这是因为lock锁是在go程序的内存只能锁住当前程序。如果是分布式的话就需要涉及分布式锁。 注意本地通过MacJmeterIrisNginx模拟分布式场景详情可见https://blog.csdn.net/weixin_45565886/article/details/136635997 package mainimport (contextgithub.com/go-redis/redis/v8github.com/kataras/iris/v12context2 github.com/kataras/iris/v12/contextmyTest/demo_home/redis_demo/distributed_lock/constantservice2 myTest/demo_home/redis_demo/distributed_lock/other_svc/servicesync
)func main() {constant.RedisCli redis.NewClient(redis.Options{Addr: localhost:6379,DB: 0,})_, err : constant.RedisCli.Set(context.TODO(), constant.AppleKey, 500, -1).Result()if err ! nil err ! redis.Nil {panic(err)}app : iris.New()xLock2 : new(sync.Mutex)app.Get(/consume, func(c *context2.Context) {xLock2.Lock()defer xLock2.Unlock()service2.GoodsService2.Consume()c.JSON(ok port:9999)})app.Listen(:9999, nil)
}分布式锁必备特性
分布式锁需要具备的特性 独占性(排他性)任何时刻有且仅有一个线程持有高可用redis集群情况下不能因为某个节点挂了而出现获取锁失败和释放锁失败的情况防死锁杜绝死锁必须有超时控制机制或撤销操作 Expire key不乱抢防止乱抢。自己只能unlock自己的锁lua脚本保证原子性且只删除自己的锁重入性同一个节点的同一个线程如果获得锁之后它也可以再次获取这个锁 setnx只能解决有无分布式锁hset 解决可重入问题记录加锁次数 hset zyRedisLock uuid:threadID 3 2 思路分析
宕机与过期 如果加锁成功之后某个Redis节点宕机该锁一直得不到释放就会导致其他Redis节点加锁失败。 加锁时需要设置过期时间 //通过lua脚本保证加锁与设置过期时间的原子性func (r *RedisLock) TryLock() bool {//通过lua脚本加锁[hincrby如果key不存在则会主动创建,如果存在则会给count数加1表示又重入一次]lockCmd : if redis.call(exists, KEYS[1]) 0 or redis.call(hexists, KEYS[1], ARGV[1]) 1 then redis.call(hincrby, KEYS[1], ARGV[1], 1) redis.call(expire, KEYS[1], ARGV[2]) return 1 else return 0 endresult, err : r.redisCli.Eval(context.TODO(), lockCmd, []string{r.key}, r.Id, r.expire).Result()if err ! nil {log.Errorf(tryLock %s %v, r.key, err)return false}i : result.(int64)if i 1 {//获取锁成功自动续期go r.reNewExpire()return true}return false
}防止误删key 锁过期时间设置30s业务逻辑假如要跑40s。30s后锁自动过期释放了其他线程加锁了。再过10s后业务逻辑走完了去释放锁就会出现把其他人的锁删除。【张冠李戴】 设置key时可带上线程id和uuid我这里以uuid演示。删除key之前要判断是否是自己的锁。如果是则unlock释放不是就return走。 func (r *RedisLock) Unlock() {//通过lua脚本删除锁//1. 查看锁是否存在如果不存在直接返回//2. 如果存在对锁进行hincrby -1操作,当减到0时表明已经unlock完成可以删除keydelCmd : if redis.call(hexists, KEYS[1], ARGV[1]) 0 then return nil elseif redis.call(hincrby, KEYS[1], ARGV[1], -1) 0 then return redis.call(del, KEYS[1]) else return 0 endresp, err : r.redisCli.Eval(context.TODO(), delCmd, []string{r.key}, r.Id).Result()if err ! nil err ! redis.Nil {log.Errorf(unlock %s %v, r.key, err)}if resp nil {fmt.Println(delKey, resp)return}
}Lua保证原子性 加锁与设置过期时间需要保证原子性。否则如果加锁成功后还没来得及设置过期时间Redis节点挂掉了就又会出现其他节点一直获取不到锁的问题。 Lua脚本保证原子性 //lock 加锁设置过期时间
if redis.call(exists, KEYS[1]) 0 or redis.call(hexists, KEYS[1], ARGV[1]) 1 then redis.call(hincrby, KEYS[1], ARGV[1], 1) redis.call(expire, KEYS[1], ARGV[2]) return 1 else return 0 end//unlock解锁delCmd : if redis.call(hexists, KEYS[1], ARGV[1]) 0 then return nil elseif redis.call(hincrby, KEYS[1], ARGV[1], -1) 0 then return redis.call(del, KEYS[1]) else return 0 end//自动续期
renewCmd : if redis.call(hexists, KEYS[1], ARGV[1]) 1 then return redis.call(expire, KEYS[1], ARGV[2]) else return 0 end可重入锁 存在一部分业务方法里还需要继续加锁。需要实现锁的可重入记录加锁的次数。Lock几次就unLock几次。 map[string]map[string]int 可通过Redis hset结构实现 # yiRedisLock redis的key
# fas421424safsfa:1 uuid线程号
# 5 加锁次数重入次数
hset yiRedisLock fas421424safsfa:1 5//通过hsethincrby 保证可重入记录加锁次数
lockCmd : if redis.call(exists, KEYS[1]) 0 or redis.call(hexists, KEYS[1], ARGV[1]) 1 then redis.call(hincrby, KEYS[1], ARGV[1], 1) redis.call(expire, KEYS[1], ARGV[2]) return 1 else return 0 enddelCmd : if redis.call(hexists, KEYS[1], ARGV[1]) 0 then return nil elseif redis.call(hincrby, KEYS[1], ARGV[1], -1) 0 then return redis.call(del, KEYS[1]) else return 0 end自动续期 相同业务耗时可能因为网络等问题而有所变化。例如我们设置分布式锁超时时间为20s但是业务因为网络问题某次耗时达到了30s这时锁就会被超时释放其他线程就能获取到锁。存在业务风险。 加锁成功之后设置自动续期启一个timer定时任务比如每10s检测一下锁有没有被释放如果没有就自动续期。 // 判断锁是否存在如果存在表明业务还未完成重新设置过期时间自动续期
renewCmd : if redis.call(hexists, KEYS[1], ARGV[1]) 1 then return redis.call(expire, KEYS[1], ARGV[2]) else return 0 end3 代码
3.1 项目结构解析 constant模块定义分布式锁名称、业务Key用于模拟扣减数据库lock模块核心模块实现分布式锁 LockTryLockUnLockNewRedisLock other_svc在其他端口启另外一个服务用于本地模拟分布式service业务类扣减商品数量其中的扣减操作涉及分布式锁main提供iris web服务
3.2 全部代码 注other_svc这里不提供与分布式锁实现无太大关系。同时为了快速演示效果部分项目结构与代码不规范。 感兴趣的朋友可以上Github查看全部代码。 Githubhttps://github.com/ziyifast/ziyifast-code_instruction/tree/main/redis_demo/distributed_lock现象 constant/const.go
package constantimport github.com/go-redis/redis/v8var (BizKey XXOOAppleKey appleRedisCli *redis.Client
)lock/redis_lock.go
package serviceimport (contextgithub.com/go-redis/redis/v8github.com/ziyifast/logmyTest/demo_home/redis_demo/distributed_lock/constantmyTest/demo_home/redis_demo/distributed_lock/lockstrconv
)type goodsService struct {
}var GoodsService new(goodsService)func (g *goodsService) Consume() {redisLock : lock.NewRedisLock(constant.RedisCli, constant.BizKey)redisLock.Lock()defer redisLock.Unlock()//consume goodsresult, err : constant.RedisCli.Get(context.TODO(), constant.AppleKey).Result()if err ! nil err ! redis.Nil {panic(err)}i, err : strconv.ParseInt(result, 10, 64)if err ! nil {panic(err)}if i 0 {log.Infof(no more apple...)return}_, err constant.RedisCli.Set(context.TODO(), constant.AppleKey, i-1, -1).Result()if err ! nil err ! redis.Nil {panic(err)}log.Infof(consume success...appleID:%d, i)
}service/goods_service.go
package serviceimport (contextgithub.com/go-redis/redis/v8github.com/ziyifast/logmyTest/demo_home/redis_demo/distributed_lock/constantmyTest/demo_home/redis_demo/distributed_lock/lockstrconv
)type goodsService struct {
}var GoodsService new(goodsService)func (g *goodsService) Consume() {redisLock : lock.NewRedisLock(constant.RedisCli, constant.BizKey)redisLock.Lock()defer redisLock.Unlock()//consume goodsresult, err : constant.RedisCli.Get(context.TODO(), constant.AppleKey).Result()if err ! nil err ! redis.Nil {panic(err)}i, err : strconv.ParseInt(result, 10, 64)if err ! nil {panic(err)}if i 0 {log.Infof(no more apple...)return}_, err constant.RedisCli.Set(context.TODO(), constant.AppleKey, i-1, -1).Result()if err ! nil err ! redis.Nil {panic(err)}log.Infof(consume success...appleID:%d, i)
}main.go
package mainimport (contextgithub.com/go-redis/redis/v8github.com/kataras/iris/v12context2 github.com/kataras/iris/v12/contextmyTest/demo_home/redis_demo/distributed_lock/constantmyTest/demo_home/redis_demo/distributed_lock/service
)func main() {constant.RedisCli redis.NewClient(redis.Options{Addr: localhost:6379,DB: 0,})_, err : constant.RedisCli.Set(context.TODO(), constant.AppleKey, 500, -1).Result()if err ! nil err ! redis.Nil {panic(err)}app : iris.New()//xLock : new(sync.Mutex)app.Get(/consume, func(c *context2.Context) {//xLock.Lock()//defer xLock.Unlock()service.GoodsService.Consume()c.JSON(ok port:8888)})app.Listen(:8888, nil)
}