添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

第七章 【高级篇】分布式锁之Redis6+Lua脚本实现原生分布式锁

第1集 分布式核心技术-关于高并发下分布式锁你知道多少?

简介:分布式锁核心知识介绍和注意事项

背景

就是保证同一时间只有一个客户端可以对共享资源进行操作

案例:优惠券领劵限制张数、商品库存超卖

核心

为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度

利用互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题

避免共享资源并发操作导致数据问题

加锁

本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题

分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper、Mysql等都可以

设计分布式锁应该考虑的东西

排他性

在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行

容错性

分布式锁一定能得到释放,比如客户端奔溃或者网络中断

满足可重入、高性能、高可用

注意分布式锁的开销、锁粒度

第2集 基于Redis实现分布式锁的几种坑你是否踩过《上》

简介:基于Redis实现分布式锁的几种坑

实现分布式锁 可以用 Redis、Zookeeper、Mysql数据库这几种 , 性能最好的是Redis且是最容易理解

分布式锁离不开 key - value 设置

key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种优惠券活动加锁,key 命名为 “coupon:id” 。value就可以使用固定值,比如设置成1

基于redis实现分布式锁,文档: redis.cn/commands.html#

加锁 SETNX key value

setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作

​如果 key 不存在,则设置当前 key 成功,返回 1;​如果当前 key 已经存在,则设置当前 key 失败,返回 0

解锁 del (key)

得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)

配置锁超时 expire (key,30s)

客户端奔溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放

综合伪代码

methodA(){

String key = "coupon_66"​ if(setnx(key,1) == 1){ expire(key,30,TimeUnit.MILLISECONDS) try { //做对应的业务逻辑 //查询用户是否已经领券 //如果没有则扣减库存 //新增领劵记录 } finally { del(key) } }else{​ /[睡]眠100毫秒,然后自旋调用本方法 methodA() }}

存在哪些问题,大家自行思考下

第3集 基于Redis实现分布式锁的几种坑你是否踩过《下》

简介:手把手教你彻底掌握分布式锁+原生代码编写

存在什么问题?

多个命令之间不是原子性操作,如setnx和expire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁

使用原子命令:设置和配置过期时间 setnx / setex

如: set key 1 ex 30 nxjava里面 redisTemplate.opsForValue().setIfAbsent("seckill_1","success",30,TimeUnit.MILLISECONDS)

业务超时,存在其他线程勿删,key 30秒过期,假如线程A执行很慢超过30秒,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没执行完成,结果就是线程A删除了线程B加的锁

可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁, 那 value 应该是存当前线程的标识或者uuid

​String key = "coupon_66"String value = Thread.currentThread().getId()​if(setnx(key,value) == 1){ expire(key,30,TimeUnit.MILLISECONDS) try { //做对应的业务逻辑 } finally { //删除锁,判断是否是当前线程加的 if(get(key).equals(value)){ //还存在时间间隔 del(key) } }}else{ /[睡]眠100毫秒,然后自旋调用本方法​}

进一步细化误删

当线程A获取到正常值时,返回带代码中判断期间锁过期了,线程B刚好重新设置了新值,线程A那边有判断value是自己的标识,然后调用del方法,结果就是删除了新设置的线程B的值

核心还是判断和删除命令 不是原子性操作导致

总结

加锁+配置过期时间:保证原子性操作

解锁: 防止误删除、也要保证原子性操作

那如何解决呢?下集讲解

第4集 手把手教你彻底掌握分布式锁lua脚本+redis原生代码编写

简介:手把手教你彻底掌握分布式锁+原生代码编写

前面说了redis做分布式锁存在的问题

核心是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用 判断和删除怎么保证原子性

文档: redis.cn/commands/set.h

多个命令的原子性:采用 lua脚本+redis, 由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败

//获取lock的值和传递的值一样,调用删除操作返回1,否则返回0

​String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";​//Arrays.asList(lockKey)是key列表,uuid是参数Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);

全部代码

/**

* 原生分布式锁 开始* 1、原子加锁 设置过期时间,防止宕机死锁* 2、原子解锁:需要判断是不是自己的锁*/@RestController@RequestMapping("/api/v1/coupon")public class CouponController {​ @Autowired private StringRedisTemplate redisTemplate;​​ @GetMapping("add") public JsonData saveCoupon(@RequestParam(value = "coupon_id",required = true) int couponId){​ //防止其他线程误删 String uuid = UUID.randomUUID().toString();​ String lockKey = "lock:coupon:"+couponId;​ lock(couponId,uuid,lockKey);​ return JsonData.buildSuccess();​ }​​ private void lock(int couponId,String uuid,String lockKey){​​ //lua脚本 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";​ Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30)); System.out.println(uuid+"加锁状态:"+nativeLock); if(nativeLock){ //加锁成功​ try{ //TODO 做相关业务逻辑 TimeUnit.SECONDS.sleep(10L);​ } catch (InterruptedException e) {​ } finally { //解锁 Long result = redisTemplate.execute( new DefaultRedisScript<>(script,Long.class),Arrays.asList(lockKey),uuid); System.out.println("解锁状态:"+result);​ }​ }else { //自旋操作 try { System.out.println("加锁失败,睡眠5秒 进行自旋"); TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { }​ //睡眠一会再尝试获取锁 lock(couponId,uuid,lockKey); } }​​}

遗留一个问题,锁的过期时间,如何实现锁的自动续期 或者 避免业务执行时间过长,锁过期了?

原生方式的话,一般把锁的过期时间设置久一点,比如10分钟时间

原生代码+redis实现分布式锁使用比较复杂,且有些锁续期问题更难处理

延伸出框架 官方推荐方式: redis.io/topics/distloc

使用特别简单: 看看公司做的工业级项目代码


发布于 2021-06-21 09:57