业务-抽奖减库存-数据库实现分布式锁 优化递归获取锁-解决库存遗留问题

接上篇: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防误删
  • 加锁和解锁必须具有原子性。(关系型数据库为我们保存原子性

 

 

posted @ 2022-07-31 22:13  KwFruit  阅读(227)  评论(0编辑  收藏  举报