业务-抽奖减库存-数据库实现分布式锁 优化递归获取锁-解决库存遗留问题
接上篇:https://www.cnblogs.com/mangoubiubiu/p/16536014.html
一、什么是库存遗留问题?
如果库存有100,100个请求过来,有50个请求成功获取锁,剩下的请求没有获取到锁,等待2次(上篇是等待2次)也没有获取到锁,这样只有50个请求能够成功执行业务代码,成功完成库存扣减,
剩下的50个请求因为没有获取到锁,没有办法执行到业务代码,所以还剩50个没有完成库存扣减,这个就是库存遗留问题。
二、递归优化,当前请求获取不到锁一直等待,直到获取到锁为止。
1、代码
递归获取锁
递归判断锁是否过期
package com.mangoubiubiu.aspect; import com.mangoubiubiu.annotation.BusinessLockAno; import com.mangoubiubiu.entities.BusinessLock; import com.mangoubiubiu.mapper.BusinessLockMapper; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import java.net.InetAddress; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; import java.util.UUID; /** * 分布式锁切面类 * @author daiyj */ @Slf4j @Aspect @Component @AllArgsConstructor public class LockAspect { private BusinessLockMapper lockMapper; /** * 切入点 */ @Pointcut("@annotation(com.mangoubiubiu.annotation.BusinessLockAno)") public void pointCut(){} /** * 环绕通知 * @param joinPoint * @return */ @Around("@annotation(businessLockAno) && pointCut()") public Object around(ProceedingJoinPoint joinPoint, BusinessLockAno businessLockAno) { Object result = null; try{ //锁的名称 String name = businessLockAno.name(); //锁的时间 String time = businessLockAno.time(); String uuid = UUID.randomUUID().toString().replace("-", ""); String hostName = InetAddress.getLocalHost().getHostName(); String currentIpAddress = InetAddress.getByName(hostName).getHostAddress(); BusinessLock lock = new BusinessLock(); lock.setName(name); lock.setId(uuid); lock.setLockedAt(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))); lock.setLockIp(currentIpAddress); lock.setLockUntil(getLockerTime(Long.valueOf(time))); //true 正常拿到锁。 false情况会一直等待获取锁,所有这里不用做处理 boolean lockStatus = this.getLock(name,lock); if(lockStatus){ //执行目标业务方法 result = joinPoint.proceed(); /** * 1,如果当A服务器先加锁,此时A服务器宕机,B服务器先判断锁是否超时,然后在加锁,此时A服务恢复过来,在来解锁,就会把B服务器的锁给删除掉。 * 2,所以这里加了UUID防止误删除。 * 3,解锁 解铃还须系铃人 解锁的人 必须是锁定者。 */ lockMapper.delLock(name,uuid,currentIpAddress); } }catch (Throwable e){ e.printStackTrace(); } //result 为目标方法的返回结果 原路返回出去 return result; } private String getLockerTime(Long time){ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); Date date = new Date(); long l = date.getTime(); long lockTime = l+time; Date date2 = new Date(); date2.setTime(lockTime); return sdf.format(date2); } private String getCurrentTime(Long time){ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); Date date = new Date(); return sdf.format(date); } /** * 获取锁 true 为锁定状态 false为解锁状态 * @param name * @return */ public boolean getLock(String name, BusinessLock businessLock) throws Exception { boolean flag = false; //通过锁的名字获取锁 BusinessLock qyrBusinessLock = lockMapper.selectByPrimaryKey(name); //判断锁是否过期 boolean expire = this.isExpire(name, qyrBusinessLock); if(expire){ flag = this.insert(businessLock); } return flag; } /** * 加锁(插入一条记录) true 为插入成功 * @param businessLock * @return */ private boolean insert(BusinessLock businessLock) throws InterruptedException { boolean flag=false; if (flag ==true){ return flag; } try { lockMapper.insert(businessLock); flag=true; }catch (Exception e){ // e.printStackTrace(); Thread.sleep(100); /** * 1. 递归调用,锁一直未加上(未插入成功),一直等待一直获取,直到锁插入成功(加锁成功),获取到锁为止。 * 2. 这里是为了解决库存遗留问题,保证客户的每一个请求都能有效的往下执行。 */ flag = insert(businessLock); } return flag; } /** * 判断锁是否过期 true 为已过期 * @param name * @param businessLock * @return * @throws ParseException */ public boolean isExpire(String name,BusinessLock businessLock)throws Exception { boolean flag = false; if (flag ==true){ return flag; } if(businessLock != null){ //判断锁是否过期 Date nowTime = new Date(); Calendar nowCalendar = Calendar.getInstance(); nowCalendar.setTime(nowTime); String lockedUntil = businessLock.getLockUntil(); SimpleDateFormat sf= new SimpleDateFormat("yyyyMMddHHmmss"); Date parse = sf.parse(lockedUntil); Calendar lockedTime = Calendar.getInstance(); lockedTime.setTime(parse); //锁定时间在当前时间之后释放锁 if(nowCalendar.after(lockedTime)){ lockMapper.deleteByPrimaryKey(name); flag = true; }else { //等待一秒再次获取 Thread.sleep(100); /** * 1. 递归调用,锁一直未过期,一直等待一直获取,直到锁过期,用户获取到锁为止 * 2. 这里是为了解决库存遗留问题,保证客户的每一个请求都能有效的往下执行。 */ flag = isExpire(name,businessLock); } }else { flag = true; } return flag; } }
三、测试
1、设置库存量有100 模拟100个请求10个并发
非常nice,我们可以看到 100个请求 10个并发下 100个库存成功完成扣减,也就是说每一个请求都扣减了自己的库存
2、加大力度,继续压测 设置库存量有100 模拟100个请求20个并发
ab -n 100 -c 20 http://192.168.10.1:8180/huas/user/test
非常不幸,我们可以看到有两个请求扣减了同一个库存,也就是说分布式锁失效了。
3、问题排查
开启sql打印 加大力度模拟100个请求 50个并发,这里我们看到终极罪魁祸首,不分青红皂白,直接根据锁名称来删除锁,让请求有机可乘,删除锁后,同时插入数据,操作同一个资源,导致分布式锁失效。
2022-07-31 22:00:21.025 DEBUG 27016 --- [io-8180-exec-11] c.m.m.B.deleteByPrimaryKey : ==> Preparing: delete from business_lock where name = ? 2022-07-31 22:00:21.025 INFO 27016 --- [io-8180-exec-52] c.m.service.impl.TempServiceImpl : http-nio-8180-exec-52测试加锁减去库存---------》49
这段代码出自哪里呢? 判断锁是否过期,然后删除主键。
4、代码优化
设置过期时间的目的是什么?是为了防止死锁的发生。这里我们在获取锁之前判断锁是否过期是在代码里判断的,sql里面的时候没有判断。我们直接在sql里面判断就行啦!!!
干掉这段代码
然后在插入前删除过期的锁
package com.mangoubiubiu.aspect; import com.mangoubiubiu.annotation.BusinessLockAno; import com.mangoubiubiu.entities.BusinessLock; import com.mangoubiubiu.mapper.BusinessLockMapper; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import java.net.InetAddress; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; import java.util.UUID; /** * 分布式锁切面类 * @author daiyj */ @Slf4j @Aspect @Component @AllArgsConstructor public class LockAspect { private BusinessLockMapper lockMapper; /** * 切入点 */ @Pointcut("@annotation(com.mangoubiubiu.annotation.BusinessLockAno)") public void pointCut(){} /** * 环绕通知 * @param joinPoint * @return */ @Around("@annotation(businessLockAno) && pointCut()") public Object around(ProceedingJoinPoint joinPoint, BusinessLockAno businessLockAno) { Object result = null; try{ //锁的名称 String name = businessLockAno.name(); //锁的时间 String time = businessLockAno.time(); String uuid = UUID.randomUUID().toString().replace("-", ""); String hostName = InetAddress.getLocalHost().getHostName(); String currentIpAddress = InetAddress.getByName(hostName).getHostAddress(); BusinessLock lock = new BusinessLock(); lock.setName(name); lock.setId(uuid); lock.setLockedAt(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))); lock.setLockIp(currentIpAddress); lock.setLockUntil(getLockerTime(Long.valueOf(time))); //true 正常拿到锁。 false情况会一直等待获取锁,所有这里不用做处理 boolean lockStatus = this.getLock(name,lock); if(lockStatus){ //执行目标业务方法 result = joinPoint.proceed(); /** * 1,如果当A服务器先加锁,此时A服务器宕机,B服务器先判断锁是否超时,然后在加锁,此时A服务恢复过来,在来解锁,就会把B服务器的锁给删除掉。 * 2,所以这里加了UUID防止误删除。 * 3,解锁 解铃还须系铃人 解锁的人 必须是锁定者。 */ lockMapper.delLock(name,uuid,currentIpAddress); } }catch (Throwable e){ e.printStackTrace(); } //result 为目标方法的返回结果 原路返回出去 return result; } private String getLockerTime(Long time){ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); Date date = new Date(); long l = date.getTime(); long lockTime = l+time; Date date2 = new Date(); date2.setTime(lockTime); return sdf.format(date2); } private String getCurrentTime(Long time){ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); Date date = new Date(); return sdf.format(date); } /** * 获取锁 true 为锁定状态 false为解锁状态 * @param name * @return */ public boolean getLock(String name, BusinessLock businessLock) throws Exception { boolean flag = false; //通过锁的名字获取锁 BusinessLock qyrBusinessLock = lockMapper.selectByPrimaryKey(name); //先删除过期的锁 lockMapper.deleteExpireLock(name); //判断锁是否过期 flag = this.insert(businessLock); return flag; } /** * 加锁(插入一条记录) true 为插入成功 * @param businessLock * @return */ private boolean insert(BusinessLock businessLock) throws InterruptedException { boolean flag=false; if (flag ==true){ return flag; } try { lockMapper.insert(businessLock); flag=true; }catch (Exception e){ // e.printStackTrace(); Thread.sleep(100); /** * 1. 递归调用,锁一直未加上(未插入成功),一直等待一直获取,直到锁插入成功(加锁成功),获取到锁为止。 * 2. 这里是为了解决库存遗留问题,保证客户的每一个请求都能有效的往下执行。 */ flag = insert(businessLock); } return flag; } // // /** // * 判断锁是否过期 true 为已过期 // * @param name // * @param businessLock // * @return // * @throws ParseException // */ // public boolean isExpire(String name,BusinessLock businessLock)throws Exception { // boolean flag = false; // if (flag ==true){ // return flag; // } //// log.info("{}测试加锁减去库存businessLock---------》{}",businessLock != null); // // if(businessLock != null){ // //判断锁是否过期 // Date nowTime = new Date(); // Calendar nowCalendar = Calendar.getInstance(); // nowCalendar.setTime(nowTime); // String lockedUntil = businessLock.getLockUntil(); // SimpleDateFormat sf= new SimpleDateFormat("yyyyMMddHHmmss"); // Date parse = sf.parse(lockedUntil); // Calendar lockedTime = Calendar.getInstance(); // lockedTime.setTime(parse); // //锁定时间在当前时间之后释放锁 // if(nowCalendar.after(lockedTime)){ // lockMapper.deleteExpireLock(name); // flag = true; // }else { // //等待一秒再次获取 // Thread.sleep(100); // /** // * 1. 递归调用,锁一直未过期,一直等待一直获取,直到锁过期,用户获取到锁为止 // * 2. 这里是为了解决库存遗留问题,保证客户的每一个请求都能有效的往下执行。 // */ // flag = isExpire(name,businessLock); // } // }else { // flag = true; // } // return flag; // } }
5、继续压测
100个库存 模拟100个请求100个并发
ab -n 100 -c 100 http://192.168.10.1:8180/huas/user/test
SUCCESS!!!成功清完所有库存
6、补充,上面的递归逻辑有点问题。。。。 修改后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | package com.mangoubiubiu.aspect; import com.mangoubiubiu.annotation.BusinessLockAno; import com.mangoubiubiu.entities.BusinessLock; import com.mangoubiubiu.mapper.BusinessLockMapper; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import java.net.InetAddress; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; import java.util.UUID; /** * 分布式锁切面类 * @author daiyj */ @Slf4j @Aspect @Component @AllArgsConstructor public class LockAspect { private BusinessLockMapper lockMapper; /** * 切入点 */ @Pointcut ( "@annotation(com.mangoubiubiu.annotation.BusinessLockAno)" ) public void pointCut(){} /** * 环绕通知 * @param joinPoint * @return */ @Around ( "@annotation(businessLockAno) && pointCut()" ) public Object around(ProceedingJoinPoint joinPoint, BusinessLockAno businessLockAno) { Object result = null ; try { //锁的名称 String name = businessLockAno.name(); //锁的时间 String time = businessLockAno.time(); String uuid = UUID.randomUUID().toString().replace( "-" , "" ); String hostName = InetAddress.getLocalHost().getHostName(); String currentIpAddress = InetAddress.getByName(hostName).getHostAddress(); BusinessLock lock = new BusinessLock(); lock.setName(name); lock.setId(uuid); lock.setLockedAt(LocalDateTime.now().format(DateTimeFormatter.ofPattern( "yyyyMMddHHmmss" ))); lock.setLockIp(currentIpAddress); lock.setLockUntil(getLockerTime(Long.valueOf(time))); //true 正常拿到锁。 false情况会一直等待获取锁,所有这里不用做处理 boolean lockStatus = this .getLock(name,lock); if (lockStatus){ //执行目标业务方法 result = joinPoint.proceed(); /** * 1,如果当A服务器先加锁,此时A服务器宕机,B服务器先判断锁是否超时,然后在加锁,此时A服务恢复过来,在来解锁,就会把B服务器的锁给删除掉。 * 2,所以这里加了UUID防止误删除。 * 3,解锁 解铃还须系铃人 解锁的人 必须是锁定者。 */ // lockMapper.delLock(name,uuid,currentIpAddress); } } catch (Throwable e){ e.printStackTrace(); } //result 为目标方法的返回结果 原路返回出去 return result; } private String getLockerTime(Long time){ SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMddHHmmss" ); Date date = new Date(); long l = date.getTime(); long lockTime = l+time; Date date2 = new Date(); date2.setTime(lockTime); return sdf.format(date2); } private String getCurrentTime(Long time){ SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMddHHmmss" ); Date date = new Date(); return sdf.format(date); } /** * 获取锁 true 为锁定状态 false为解锁状态 * @param name * @return */ public boolean getLock(String name, BusinessLock businessLock) throws Exception { boolean flag = false ; //通过锁的名字获取锁 BusinessLock qyrBusinessLock = lockMapper.selectByPrimaryKey(name); //先删除过期的锁 //判断锁是否过期 flag = this .insert(businessLock, false ); return flag; } /** * 加锁(插入一条记录) true 为插入成功 * @param businessLock * @return */ private boolean insert(BusinessLock businessLock, boolean flag) throws InterruptedException { lockMapper.deleteExpireLock(businessLock.getName()); if (flag == true ){ return flag; } try { lockMapper.insert(businessLock); flag= true ; } catch (Exception e){ // e.printStackTrace(); Thread.sleep( 100 ); /** * 1. 递归调用,锁一直未加上(未插入成功),一直等待一直获取,直到锁插入成功(加锁成功),获取到锁为止。 * 2. 这里是为了解决库存遗留问题,保证客户的每一个请求都能有效的往下执行。 */ flag = insert(businessLock,flag); } return flag; } // // /** // * 判断锁是否过期 true 为已过期 // * @param name // * @param businessLock // * @return // * @throws ParseException // */ // public boolean isExpire(String name,BusinessLock businessLock)throws Exception { // boolean flag = false; // if (flag ==true){ // return flag; // } //// log.info("{}测试加锁减去库存businessLock---------》{}",businessLock != null); // // if(businessLock != null){ // //判断锁是否过期 // Date nowTime = new Date(); // Calendar nowCalendar = Calendar.getInstance(); // nowCalendar.setTime(nowTime); // String lockedUntil = businessLock.getLockUntil(); // SimpleDateFormat sf= new SimpleDateFormat("yyyyMMddHHmmss"); // Date parse = sf.parse(lockedUntil); // Calendar lockedTime = Calendar.getInstance(); // lockedTime.setTime(parse); // //锁定时间在当前时间之后释放锁 // if(nowCalendar.after(lockedTime)){ // lockMapper.deleteExpireLock(name); // flag = true; // }else { // //等待一秒再次获取 // Thread.sleep(100); // /** // * 1. 递归调用,锁一直未过期,一直等待一直获取,直到锁过期,用户获取到锁为止 // * 2. 这里是为了解决库存遗留问题,保证客户的每一个请求都能有效的往下执行。 // */ // flag = isExpire(name,businessLock); // } // }else { // flag = true; // } // return flag; // } } |
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。(数据库主键保证只能有一个客户端持有锁)
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。(过期时间)
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。(uuid防误删)
- 加锁和解锁必须具有原子性。(关系型数据库为我们保存原子性)
本文作者:KwFruit
本文链接:https://www.cnblogs.com/mangoubiubiu/p/16538381.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步