写在前面
最近在工作中遇到了一个问题,多个用户对某一条数据进行操作时,都操作成功了,违背了当初需求规定的唯一性,经过分析排查,总结下来一共有两点原因:
- 没有对数据的状态进行校验。
- 在集群情况下,没有使用锁来保证同步。
然后我就不得不对这一功能进行修复了。本来工程里面已经有了对应的分布式锁的调用方法,但是技术负责人要求先用本地锁再用分布式锁。为什么要这么用呢,因为工作行业的特殊性,规定不让用Redis等中间件来做分布式锁。没错,我们用的就是数据库。所以,如果每台机器都有多个线程来同时争抢分布式锁的话,那么对数据库的要求就比较高了(PS:现在数据库感觉已经不堪重负了)。经过漫长的思考,自己终于有了一些思路。接下来,让我们来看看我是怎么实现的吧(请各位大佬轻点喷,毕竟我只是个初级工程师)。
加锁
//使用map来进行本地锁
Map<String, Thread> LOCK_MAP = new ConcurrentHashMap<>();
//使用ThreadLocal防止并发修改
ThreadLocal<Boolean> LOCK_FLAG = new ThreadLocal<>();
//实际应用中,可以考虑将重试参数弄成配置化,方便根据系统负载进行动态调整
private final int RETRY_TIME = 10;
@Autowired
private LockMapper lockMapper;
@Override
public boolean lock(String businessType, long time,String timeUnit) throws InterruptedException {
boolean flag = false;
int retryTime = RETRY_TIME;
do{
Thread thread = LOCK_MAP.putIfAbsent(businessType, Thread.currentThread());
//表明本地加锁成功,可以继续尝试加分布式锁
if(Thread.currentThread() == thread)
flag = lockMapper.lock(businessType, time, timeUnit) > 0;
if(flag){
//加锁成功,则退出当前循环
break;
}
//睡眠,防止一直进行争抢锁导致CPU升高,并且也给真实的业务处理留一些时间
Thread.sleep(500);
}while (retryTime > 0);
return flag;
}
如上的代码其实就实现了想要的那种效果,只所以使用map来进行加锁是因为java中的锁不支持根据类型锁定,咱们在实际的业务过程中肯定是要根据不同的业务类型来进行锁定的,各个业务之间应该互不影响。
其实上方的代码最开始我是使用了Synchronized来防止并发修改,但是这样出现了一个问题那就是我本来就是为了抢锁,但是我抢锁之前还要再去争抢一次抢锁的锁(同一种业务类型抢锁会出现同步等待,但是其实不应该影响),这干的都不是人事(真的是极大的降低了并发)。
还有值得一说的就是,这里面我就没有把对应的具体使用分布式锁的SQL给列出来,为什么呢,当然是因为公司不让(其实是因为我真的没看那个)。各位如果感兴趣,请各位自行实现。
释放锁
public void unLock(String businessType) {
Thread thread = LOCK_MAP.get(businessType);
if(Thread.currentThread() == thread){
//加锁是自己,则可以释放锁
lockMapper.unLock(businessType);
LOCK_MAP.remove(businessType);
}
}
对比起来,释放锁的逻辑简单多了,其实就是从map中获取一下,判断是不是当前线程,如果是,则进行释放锁,如果不是则退出。
写在最后
小弟学识浅薄,欢迎各位大佬进行批评指正,将对您报以诚挚的谢意。