业务-抽奖减库存-数据库实现分布式锁 优化递归获取锁-解决库存遗留问题
接上篇: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、补充,上面的递归逻辑有点问题。。。。 修改后
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防误删)
- 加锁和解锁必须具有原子性。(关系型数据库为我们保存原子性)