分布式锁的解决
1、分布式锁的使用场景
- 秒杀类的
- 接口幂等性
2、秒杀问题现象
通过Nginx 配置了负载,访问后台的接口,接口里面减库存代码如下
@RestController public class RedisController { @Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping("/deductStock") public String deductStock(){ int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock>0){ int realStock = stock - 1; stringRedisTemplate.opsForValue().set("stock",realStock+""); System.out.println("库存扣减成功、剩余库存:"+realStock); }else { System.out.println("扣减失败库存不足"); } return "end"; } }
测试结果如下
可以发现再并发的情况下会出现超卖的情况。
3、使用原生Redis的方式解决
/** * 使用redis 做分布式锁 * 1、Redis 是单线程还是多线程---->单线程 * */ @RestController public class RedisController { @Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping("/deductStock") public String deductStock(){ String stockLock = "stockLock"; String clientId = UUID.randomUUID().toString(); try{ /** setIfAbsent 的意思是 如果key存在的话,那么设置值是无法成功的 Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(stockLock,"gaohq"); 可以给锁设置一个超时时间(10秒),就算是jvm宕机了 这个锁也可以释放,这里也要特别的注意啊, 上面的加锁了后,下面的设置超时时间没有执行,jvm 宕机了。也会造成死锁 也就是需要设置值和设置超时时间为一个原子操作才行, stringRedisTemplate.expire(stockLock,10, TimeUnit.SECONDS); 这里设置value的时候不要写死了,如果第一个请求过来 耗时15秒,第二个请求过来耗时10秒 第一个请求到 if(stock>0) 这里耗了10秒了 剩下的代码执行需要 5秒,此时value 已经超时了, 第二个线程就进来了,获得了锁。在finally 里面判断下。 这个时间设置多久呢 设置多久都不合适、需要锁的续命的方式。 手写太麻烦,用Redission 就行。 **/ //下面这行代码是原子操作 也就是说放值和设置超时时间是要一个原子操作。 //Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(stockLock,"gaohq",10,TimeUnit.SECONDS); Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(stockLock,clientId,10,TimeUnit.SECONDS); if(!result){ return "error code"; } int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock>0){ int realStock = stock - 1; stringRedisTemplate.opsForValue().set("stock",realStock+""); System.out.println("库存扣减成功、剩余库存:"+realStock); }else { System.out.println("扣减失败库存不足"); } //如果这里获取到了锁后,然后突然程序被kill 掉了,那么finally里面也就不能执行了。造成死锁 }finally { /** * 如果上面的代码抛异常了,那么stockLock 这个锁就一直存在存在死锁,那么就扣不了库存了 * 像这种关闭链接、释放锁、关闭流 一般都放到finally 里面。 * 释放锁的时候要判断下,当前请求也就是线程 释放的锁是否是当前线程拿到的,避免因为时间的问题 * 把别的线程的拿到的锁释放了。 * * * 释放锁,涉及到三个步骤,获取锁、比较锁、释放锁三个步骤,这个三个步骤明显不是一个原子操作,可以使用lua脚本 * 在redis的服务端原子性的执行多个redis的命令。 * */ if(clientId.equals(stringRedisTemplate.opsForValue().get("stockLock"))){ stringRedisTemplate.delete(stockLock); } } return "end"; } }
4、Redisson 的方式
4.1 Redisson的配置类
/** * * 这个类用来把一些不在springboot自动包扫描的路径下的类,放到容器里面。 * */ @Import({Person.class, Book.class, AgeMyServiceImpl.class, QueryGrantTypeService.class}) //@ComponentScan("com.ali.gits.beans") @Configuration public class GaohqClassConfig { @Value("${redis.hostname}") String hostName; @Value("${redis.port}") int port; @Value("${spring.redis.password}") String password; @Bean(destroyMethod="shutdown") RedissonClient redisson() throws IOException { //1、创建配置 Config config = new Config(); config.useSingleServer() .setAddress("redis://"+hostName+":"+port).setPassword(password); return Redisson.create(config); } }
4.2 使用Redisson
/** * * 使用Redission 解决分布式锁的问题。 * */ @RestController public class RedissionController { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private RedissonClient redissonClient; @RequestMapping("/deductStockByRedisson") public String deductStock(){ String stockLock = "stockLock"; RLock rLock = redissonClient.getLock(stockLock); rLock.lock(); try{ //如果业务执行过长,Redisson会自动给锁续期 int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock>0){ int realStock = stock - 1; stringRedisTemplate.opsForValue().set("stock",realStock+""); System.out.println("库存扣减成功、剩余库存:"+realStock); }else { System.out.println("扣减失败库存不足"); } //如果这里获取到了锁后,然后突然程序被kill 掉了,那么finally里面也就不能执行了。造成死锁 }finally { rLock.unlock(); } return "end"; } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)