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

接上篇: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 中国大陆许可协议进行许可。

posted @   KwFruit  阅读(240)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起