第十五章 原生分布式锁-Redisson分布式锁防止个人超领优惠券
一行代码10个bug-单用户超领优惠券问题介绍
简介:讲解单用户优惠券超领业务问题和效果演示
-
什么单用户超领优惠券
- 优惠券限制1人限制1张,有些人却领了2张
- 优惠券限制1人限制2张,有些人却领了3或者4张
-
案例举例和问题来源
前面解决了,优惠券超发的问题,但是这个个人领取的时候,有张数限制,
有个生发洗发水100元,有个10元优惠券,每人限制领劵1张
小滴课堂-老王,使用时间暂停思维来发现问题,并发领劵
A线程原先查询出来没有领劵,要再插入领劵记录前暂停
然后B线程原先查询出来也没有领劵,则插入领劵记录,然后A线程也插入领劵记录
老王就有了两个优惠券
问题来源核心:对资源的修改没有加锁,导致多个线程可以同时操作,从而导致数据不正确
解决问题:分布式锁 或者 细粒度分布式锁
第2集 分布式核心技术-关于高并发下分布式锁你知道多少?
简介:分布式锁核心知识介绍和注意事项
- 避免单人超领劵
- 加锁
- 本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题
- 分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper、Mysql等都可以
- 加锁
- 设计分布式锁应该考虑的东西
- 排他性
- 在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
- 容错性
- 分布式锁一定能得到释放,比如客户端奔溃或者网络中断
- 满足可重入、高性能、高可用
- 注意分布式锁的开销、锁粒度
- 排他性
基于Redis实现分布式锁的几种坑你是否踩过《上》
简介:基于Redis实现分布式锁的几种坑
-
实现分布式锁 可以用 Redis、Zookeeper、Mysql数据库这几种 , 性能最好的是Redis且是最容易理解
- 分布式锁离不开 key - value 设置
key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种商品的秒杀活动加锁,key 命名为 “seckill_商品ID” 。value就可以使用固定值,比如设置成1
-
基于redis实现分布式锁,文档:http://www.redis.cn/commands.html#string
- 加锁 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() } }
- 存在哪些问题,大家自行思考下
第4集 基于Redis实现分布式锁的几种坑你是否踩过《下》
简介:手把手教你彻底掌握分布式锁+原生代码编写
-
-
存在什么问题?
- 多个命令之间不是原子性操作,如
setnx
和expire
之间,如果setnx
成功,但是expire
失败,且宕机了,则这个资源就是死锁
使用原子命令:设置和配置过期时间 setnx / setex 如: set key 1 ex 30 nx java里面 redisTemplate.opsForValue().setIfAbsent("seckill_1",1,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的值
- 核心还是判断和删除命令 不是原子性操作导致
-
那如何解决呢?下集讲解
- 多个命令之间不是原子性操作,如
-
手把手教你彻底掌握分布式锁lua脚本+redis原生代码编写
简介:手把手教你彻底掌握分布式锁+原生代码编写
-
前面说了redis做分布式锁存在的问题
- 核心是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用 判断和删除怎么保证原子性
- 文档:http://www.redis.cn/commands/set.html
- 多个命令的原子性:采用 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、原子解锁:需要判断是不是自己的锁 */ String uuid = CommonUtil.generateUUID(); String lockKey = "lock:coupon:"+couponId; Boolean nativeLock=redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30)); if(nativeLock){ //加锁成功 log.info("加锁:{}",nativeLock); try { //执行业务 TODO }finally { String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid); log.info("解锁:{}",result); } }else { //加锁失败,睡眠100毫秒,自旋重试 try { TimeUnit.MILLISECONDS.sleep(100L); } catch (InterruptedException e) { } return addCoupon( couponId, couponCategory); } //原生分布式锁 结束
- 遗留一个问题,锁的过期时间,如何实现锁的自动续期 或者 避免业务执行时间过长,锁过期了?
- 原生方式的话,一般把锁的过期时间设置久一点,比如10分钟时间
第6集 基于Redis官方推荐-分布式锁最佳实践介绍
简介:redis官方推荐-分布式锁最佳实践
-
原生代码+redis实现分布式锁使用比较复杂,且有些锁续期问题更难处理
- 官方推荐方式:https://redis.io/topics/distlock
- 多种实现客户端框架
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zt88LrLY-1665481154540)(…/…/…/Library/Application Support/typora-user-images/image-20210209234838064.png)]
- Redisson官方中文文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
-
聚合工程锁定版本,common项目添加依赖(多个服务都会用到分布式锁)
<!--分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.1</version>
</dependency>
- 创建redisson客户端
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
@Value("${spring.redis.password}")
private String redisPwd;
/**
* 配置分布式锁
* @return
*/
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
//单机模式
//config.useSingleServer().setPassword("123456").setAddress("redis://8.129.113.233:3308");
config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);
//集群模式
//config.useClusterServers()
//.setScanInterval(2000)
//.addNodeAddress("redis://10.0.29.30:6379", "redis://10.0.29.95:6379")
// .addNodeAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
- 模拟controller接口测试
实战Redisson实现优惠券微服务领劵接口的分布式锁
简介:redisson实现优惠券微服务领劵接口的分布式锁
- 优惠券微服务,分布式锁实现方式
Lock lock = redisson.getLock("lock:coupon:"+couponId);
//阻塞式等待,一个线程获取锁后,其他线程只能等待,和原生的方式循环调用不一样
lock.lock();
try {
CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>().eq("id", couponId)
.eq("category", couponCategory)
.eq("publish", CouponPublishEnum.PUBLISH));
this.couponCheck(couponDO,loginUser.getId());
CouponRecordDO couponRecordDO = new CouponRecordDO();
BeanUtils.copyProperties(couponDO,couponRecordDO);
couponRecordDO.setCreateTime(new Date());
couponRecordDO.setUseState(CouponStateEnum.NEW.name());
couponRecordDO.setUserId(loginUser.getId());
couponRecordDO.setUserName(loginUser.getName());
couponRecordDO.setCouponId(couponId);
couponRecordDO.setId(null);
//高并发下扣减劵库存,采用乐观锁,当前stock做版本号,一次只能领取1张
int rows = couponMapper.reduceStock(couponId);
if(rows == 1){
//库存扣减成功才保存
couponRecordMapper.insert(couponRecordDO);
}else {
log.warn("发放优惠券失败:{},用户:{}",couponDO,loginUser);
throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
}
}finally {
lock.unlock();
}
Redisson是怎样解决分布式锁的里面的坑
简介:redisson解决分布式锁里面的坑
-
问题 : Redis锁的过期时间小于业务的执行时间该如何续期?
- watch dog看门狗机制
负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。或者业务执行时间过长导致锁过期, 为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。 Redisson中客户端一旦加锁成功,就会启动一个watch dog看门狗。watch dog是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间 默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定
- 指定加锁时间
// 加锁以后10秒钟自动解锁 // 无需调用unlock方法手动解锁 lock.lock(10, TimeUnit.SECONDS); // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { ... } finally { lock.unlock(); } }