# 一般并发 解决方案 **Repository Path**: lslands/concurrent ## Basic Information - **Project Name**: 一般并发 解决方案 - **Description**: 读多写少的情况 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-03-28 - **Last Updated**: 2021-03-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 一般并发 解决方案 #### 介绍 读多写少的情况 #### 软件架构 软件架构说明 #### 安装教程 1. xxxx 2. xxxx 3. xxxx #### 使用说明 # **高并发** **模拟一个场景**,(从redis里面获取库存,然后判断库存是否0,如果不为0,那么库存减一,在set进redis) ```yaml public class DemoController { @Autowired private Redisson redisson; @Autowired private StringRedisTemplate redisTemplate; @GetMapping("/testDemo") public String testDemo(){ //获取库存值 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if (stock>0){ int realStock = stock - 1; redisTemplate.opsForValue().set("stock",realStock+""); System.out.println("扣减成功,剩余:"+realStock); }else { System.out.println("扣减失败"); } return "end"; } } ``` ## 问题一: **如上代码,如果同时涌入大量用户操作,同时操作** `int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));`` **那么,就会出现库存在同一次操作中只会减一,就会出现,操作与实际增减量不符**。 ### 解决问题一: ``` @GetMapping("/testDemo") public String testDemo(){ //增加一个synchronized 同步锁, synchronized (this){ //获取库存值 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if (stock>0){ int realStock = stock - 1; redisTemplate.opsForValue().set("stock",realStock+""); System.out.println("扣减成功,剩余:"+realStock); }else { System.out.println("扣减失败"); } } return "end"; } ``` 这种解决方案,只适合在单机环境 ## 问题二: **在问题一的基础上,如果存在分布式,那么一样会存在,问题一所述问题。synchronized的锁只是控制在jvm进程级别。** ### 解决问题二: **redis命令** **set 和setnx 区别** **使用set 插入数据时,后面的set会覆盖掉前面的数据;** **使用setnx插入数据时,如果插入数据的key存在,则不会执行。** ``` /** * 实现一个分布式锁 * @return */ @GetMapping("/testDemo1") public String testDemo1(){ String lockKey = "lockKey"; //redisTemplate.opsForValue().setIfAbsent("lockKey","lslands") == jedis.setnx(k,v) Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"lslands"); if (!result){ //此处 ,如果操作失败,就返回,或者等待等。自己处理逻辑 return "error_code"; } //获取库存值 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if (stock>0){ int realStock = stock - 1; redisTemplate.opsForValue().set("stock",realStock+""); System.out.println("扣减成功,剩余:"+realStock); }else { System.out.println("扣减失败"); } //释放锁 redisTemplate.delete(lockKey); return "end"; } ``` redis是单线程执行, ` Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"lslands");` 因此这里只会由一个人会拿到锁,此时就需要处理后面没有拿到锁的逻辑 ## 问题三: **如果,第一个线程进来拿到锁,但是执行业务的时候抛异常了,那么锁就得不到释放,后面的线程就没法进入,就造成死锁。** ### 解决问题三: ``` try { }finally { } 异常代码快, finally代码快语句,无论如何都会执行, ``` ``` @GetMapping("/testDemo1") public String testDemo1(){ String lockKey = "lockKey"; //redisTemplate.opsForValue().setIfAbsent("lockKey","lslands") == jedis.setnx(k,v) Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"lslands"); if (!result){ //此处 ,如果操作失败,就返回,或者等待等。自己处理逻辑 return "error_code"; } try { //获取库存值 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if (stock>0){ int realStock = stock - 1; redisTemplate.opsForValue().set("stock",realStock+""); System.out.println("扣减成功,剩余:"+realStock); }else { System.out.println("扣减失败"); } }finally { //finally 代码快一定会被执行 //释放锁 redisTemplate.delete(lockKey); } return "end"; } ``` 解决程序异常的问题。 ## 问题四: **问题3解决了,此时如果一同宕机了呢。那依然会出现死锁** ### 问题四解决: **思路: 设置超时时间** ``` @GetMapping("/testDemo1") public String testDemo1(){ String lockKey = "lockKey"; //redisTemplate.opsForValue().setIfAbsent("lockKey","lslands") == jedis.setnx(k,v) Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"lslands"); //设置一个超时时间,防止宕机情况下出现死锁。 让lockKey这把锁,在redis里面最多存活10秒 redisTemplate.expire(lockKey,10, TimeUnit.SECONDS); if (!result){ //此处 ,如果操作失败,就返回,或者等待等。自己处理逻辑 return "error_code"; } try { //获取库存值 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if (stock>0){ int realStock = stock - 1; redisTemplate.opsForValue().set("stock",realStock+""); System.out.println("扣减成功,剩余:"+realStock); }else { System.out.println("扣减失败"); } }finally { //finally 代码快一定会被执行 //释放锁 redisTemplate.delete(lockKey); } return "end"; } ``` ![1616919408452](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1616919408452.png) **如果设置超时时间之前,系统就宕机了,那么同样会出现死锁。** **思路:把锁和设置超时时间,原子执行(同步)** 需要把,获取锁和设置超时时间合并执行(原子执行) `redisTemplate.opsForValue().setIfAbsent(lockKey,"lslands",10, TimeUnit.SECONDS);` ``` String lockKey = "lockKey"; /* //redisTemplate.opsForValue().setIfAbsent("lockKey","lslands") == jedis.setnx(k,v) Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,"lslands"); //设置一个超时时间,防止宕机情况下出现死锁。 让lockKey这把锁,在redis里面最多存活10秒 redisTemplate.expire(lockKey,10, TimeUnit.SECONDS);*/ //原子命令。 防止在设置超时时间之前 ,系统宕机出现死锁的情况 final Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "lslands", 10, TimeUnit.SECONDS); if (!result){ //此处 ,如果操作失败,就返回,或者等待等。自己处理逻辑 return "error_code"; } ``` ## 问题五: **在问题四的基础上,如果设置超时时间为10秒,但在try里面的逻辑执行时,程序没有报错,就因为某些原因,程序逻辑块执行了15秒,那么就会出现线程一还没执行完成,线程二就进来执行了,或者线程一执行15秒,但线程只执行5秒,那么最终释放的锁可能会让是线程二的锁。按照这个逻辑,因此,这把锁还是会失效一段时间**。 **问题的根本就是,自己加的锁,可能会被别人释放** ### 解决问题五: ``` @GetMapping("/testDemo2") public String testDemo2(){ String lockKey = "lockKey"; String clientId = UUID.randomUUID().toString(); final Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,clientId, 10, TimeUnit.SECONDS); if (!result){ //此处 ,如果操作失败,就返回,或者等待等。自己处理逻辑 return "error_code"; } try { //获取库存值 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if (stock>0){ int realStock = stock - 1; redisTemplate.opsForValue().set("stock",realStock+""); System.out.println("扣减成功,剩余:"+realStock); }else { System.out.println("扣减失败"); } }finally { //在删除锁的时候, 判断一下当前的clientId 与加锁的线程ID是否时同一个 if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){ //finally 代码快一定会被执行 //释放锁 redisTemplate.delete(lockKey); } } return "end"; } ``` ## 问题六: 在问题五的基础上:出现如下问题 ``` finally { //在删除锁的时候, 判断一下当前的clientId 与加锁的线程ID是否时同一个 if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){ //finally 代码快一定会被执行 //释放锁 redisTemplate.delete(lockKey); } 如果在if判断后,突然出现如卡顿的现象时,如:在执行redisTemplate.delete(lockKey)时出现卡顿10秒,但是上面的超时时间为10秒。 此时依然会出现问题四的情况 ``` ### 解决问题六: ****Redisson 分布式锁 实现锁续命** ``` @Bean public Redisson redisson() { //单机 Config config = new Config(); /* * useSingleServer 单机 * useClusterServers 集群 * useMasterSlaveServers 主从 * useReplicatedServers 复制 * useSentinelServers 哨兵模式 * */ config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0); return (Redisson) Redisson.create(config); } ``` ### ![1616921697984](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1616921697984.png) ``` public String testDemo2(){ String lockKey = "lockKey"; //获取锁对象 RLock rLock = redisson.getLock(lockKey); try { //加锁 底层封装了 setIfAbsent(lockKey, "lslands", 10, TimeUnit.SECONDS) rLock.lock(); //获取库存值 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if (stock>0){ int realStock = stock - 1; redisTemplate.opsForValue().set("stock",realStock+""); System.out.println("扣减成功,剩余:"+realStock); }else { System.out.println("扣减失败"); } }finally { //解锁 rLock.unlock(); } return "end"; } ``` ## 问题七: **基本可以用了。 但是 这个 只正对在一个redis 里面,如果出现多太redis集群,那么同样会出现以上问题。** **并且此方法效率低。 他是一个串行,不是一个并行。** ### 解决问题七: #### 解决性能问题: ##### **1.读写锁,根据情况使用读锁还是写锁** ​ *这种只适合 读多 写少的场景* ​ **读锁 readLock** ``` public String getDemo(){ String lockKey = "lockKey"; //获取读锁 RReadWriteLock rReadWriteLock = redisson.getReadWriteLock(lockKey); //获取锁对象 RLock rLock = rReadWriteLock.readLock(); try { //加读锁 底层封装了 setIfAbsent(lockKey, "lslands", 10, TimeUnit.SECONDS) rLock.lock(); //获取库存值 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if (stock>0){ int realStock = stock - 1; redisTemplate.opsForValue().set("stock",realStock+""); System.out.println("扣减成功,剩余:"+realStock); }else { System.out.println("扣减失败"); } }finally { //释放读解锁 rLock.unlock(); } return "end"; } ``` ​ **写锁** writeLock ``` public String updateDemo(){ String lockKey = "lockKey"; //获取锁对象 RReadWriteLock rReadWriteLock = redisson.getReadWriteLock(lockKey); //获取写锁 RLock rLock = rReadWriteLock.writeLock(); try { //加读锁 底层封装了 setIfAbsent(lockKey, "lslands", 10, TimeUnit.SECONDS) rLock.lock(); //获取库存值 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if (stock>0){ int realStock = stock - 1; redisTemplate.opsForValue().set("stock",realStock+""); System.out.println("扣减成功,剩余:"+realStock); }else { System.out.println("扣减失败"); } }finally { //释放读解锁 rLock.unlock(); } return "end"; } ``` ##### #### 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request #### 特技 1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md 2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) 3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) 6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)