一直以来想写一篇关于锁的总结,因为java的锁太多了,五花八门,并且有时候不知道怎么去选择,好多人都犯糊涂,当然自己也犯糊涂。本章并不是介绍java中的concurrent包下的相关锁,那个一般在开发中间件系统中常用,更多的人开发是在和数据库打交道,接触的都是数据库级别的锁。废话不多说,直接开始。
1、悲观锁
悲观锁在数据库中,一般都是这样的select ... from xxx where id = xxx for update
这种锁,一上来就直接锁住了一条语句,并发效率极其差,如果业务代码处理稍微复杂一点基本上数据库很快就挂了,会报一大堆cant get connect ,这种肯定不会用的。
2、乐观锁
这种锁有很多公司都在用,比如说京东,一般来说分为两步。
第一步,select ... from xxx where id = xxx
第二步,update xxx set ... where id = xxx and xxx=....
说白了就是先去捞取数据,然后带上条件去更新。如果此时产生并发,那么更新就会只有一个成功,其他的都是失败,也就是返回一个0。这时候我可以选择重新查询继续尝试,或者直接返回异常。
3、分布式锁(redis)
这个锁其实一般都是用redis来做的,算是悲观锁的变种,但是好一点是锁内存,只要共用一个redis集群的系统都可以对其可见。也有很多公司喜欢用,比如说网易。一般来说也是分为两步。
第一步,jedis.set(key, value, "NX", "EX", seconds)
第二步,jedis.del(keys)
首先,先设置一个key,然后赋值value,并且设置超时时间。如果此时出现并发,那么只要key没有被删掉,那么只有一个线程会返回true,其他的都返回false,这就可以做相应的处理。但是一定要注意,要删除jeids的key,这个好多人都会犯错误,我也犯过好多次。我想此时,会有小伙伴说,加上finally啊,在finally里面删除。我一开始也是这么想的,后来发现犯了一个非常严重的错误,因为redis的锁毕竟不是真正的锁,它任何时候都是返回值。假设一个用户进入锁了,没有释放,另一个用户进来返回false,那也会执行finally块,这样的话锁就没意义了。所以这也是比较坑的地方,并且代码风格非常不好,并且内存当做锁非常不安全,但是相比1,2两种锁已经很不错了。
4、支付宝的常用锁
在介绍支付宝的锁之前先说一件事,我在加入支付宝之前,也对它的锁感兴趣,一直在想究竟支持如此高并发的场景的公司的锁到底长什么样子。所以当我刚到公司就迫不及待的看源代码,支付宝关于锁的处理做成了一个工具类,很方便大家调用,里面只用了不到50行代码实现一个锁。
支付宝的锁的核心是悲观锁!是的,你没有听错,我看到以后很不解,我问了下我们组一个特别吊的大牛---司令。我问他为什么要这样实现?如果说用乐观锁不是更好么?他问我你要是用乐观锁你怎么处理?先查然后更新,然后不匹配你怎么办?我说就抛异常啊!他说,好的,那么如果说你的业务比较复杂,执行到这一步花费了很多时间呢?你并不是想立即抛异常,而是希望重试几次呢?我说这个可以用一个for循环,更新失败就sleep,重试几次。他说,可以,那么如果你用成千上万个线程呢?这时候都在for,你的数据库是不是挂了?我那时恍然大悟。那么支付宝的锁到底怎么实现的呢?
b = false;
for(重试次数) {
try {
select .. from xx where id = xxx for update nowait;
获取锁成功,b=true; 直接break;
} catch() {
异常捕获;
}
sleep(ms)
}
if(!b) {
throw new Exception(xxxx);
}
他给我解释到:首先获取锁,因为有nowait关键字,那么锁会立即返回,如果没有获取到,在并发的时候会抛异常,然后证明抢锁激烈那么就sleep几毫秒,然后重试抢锁,如果抢到了就直接跳出循环。如果重试次数超过一定次数,那么证明此时抢锁特别激烈,那么直接抛异常。
他说,我们要知道数据库的连接是非常宝贵的,在用完以后一定要及时回收,抛异常是最快的回收链接方式,所以,支付宝的代码有很多的异常种类。虽然乐观锁看起来很清新,但是它长时间占用着链接且在开发复杂业务有个弊端,往往写代码的人都无法分辨什么时候该抛异常,导致将业务代码处理的很乱,经常造成死锁。这样看来,带nowait的悲观锁真的是很好,并且配合上事务也不用像redis那样自己要删除锁,导致经常出错,而且比较数据库级别的锁比内存稳定多了啊!
这就是本章介绍的几种锁,希望能给大家带来些收获!