前言
提到锁大家肯定有了解,像synchronized
、ReentrantLock
,在单进程情况下,多个线程访问同一资源,可以用它们来保证线程的安全性。不过目前互联网项目越来越多的项目采用集群部署,也就是分布式情况,这两种锁就有些不够用了。来两张图举例说明下,
本地锁的情况下:
分布式锁情况下:
就其思想来说,就是一种“我全都要”的思想,所有服务都到一个统一的地方来取锁,只有取到锁的才能继续执行下去。
说完思想,下面来说一下具体的实现。
redis实现
为实现分布式锁,在redis中存在SETNX key value
命令,意为set if not exists(如果不存在该key,才去set值)
,就比如说是张三去上厕所,看厕所门锁着,他就不进去了,厕所门开着他才去。
可以看到,第一次set返回了1,表示成功,但是第二次返回0,表示set失败,因为已经存在这个key了。
当然只靠setnx
这个命令可以吗?当然是不行的,试想一种情况,张三在厕所里,但他在里面一直没有释放,一直在里面蹲着,那外面人想去厕所全部都去不了,都想锤死他了。redis同理,假设已经进行了加锁,但是因为宕机或者出现异常未释放锁,就造成了所谓的“死锁”。
聪明的你们肯定早都想到了,为它设置过期时间不就好了,可以SETEX key seconds value
命令,为指定key设置过期时间,单位为秒。但这样又有另一个问题,我刚加锁成功,还没设置过期时间,redis宕机了不就又死锁了,所以说要保证原子性吖,要么一起成功,要么一起失败。当然我们能想到的redis肯定早都为你实现好了,在Redis 2.8 的版本后,redis就为我们提供了一条组合命令SET key value ex seconds nx
,加锁的同时设置过期时间。
就好比是公司规定每人最多只能在厕所呆2分钟,不管释放没释放完都得出来,这样就解决了“死锁”问题。
但这样就没有问题了吗?怎么可能。
试想又一种情况,厕所门肯定只能从里面开啊,张三上完厕所后张四进去锁上门,但是外面人以为还是张三在里面,而且已经过了3分钟了,就直接把门给撬开了,一看里面却是张四,这就很尴尬啊。换成redis就是说比如一个业务执行时间很长,锁已经自己过期了,别人已经设置了新的锁,但是当业务执行完之后直接释放锁,就有可能是删除了别人加的锁,这不是乱套了吗。
所以在加锁时候,要设一个随机值,在删除锁时进行比对,如果是自己的锁,才删除。多说无益,烦人,直接上代码。
//基于jedis和lua脚本来实现
privatestaticfinal String LOCK_SUCCESS = "OK";
privatestaticfinal Long RELEASE_SUCCESS = 1L;
privatestaticfinal String SET_IF_NOT_EXIST = "NX";
privatestaticfinal String SET_WITH_EXPIRE_TIME = "PX";
@Override
public String acquire() {
try {
// 获取锁的超时时间,超过这个时间则放弃获取锁
long end = System.currentTimeMillis() + acquireTimeout;
// 随机生成一个 value
String requireToken = UUID.randomUUID().toString();
while (System.currentTimeMillis() < end) {
String result = jedis
.set(lockKey, requireToken, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return requireToken;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (Exception e) {
log.error("acquire lock due to error", e);
}
returnnull;
}
@Override
public boolean release(String identify) {
if (identify == null) {
returnfalse;
}
//通过lua脚本进行比对删除操作,保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = new Object();
try {
result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(identify));
if (RELEASE_SUCCESS.equals(result)) {
log.info("release lock success, requestToken:{}", identify);
returntrue;
}
} catch (Exception e) {
log.error("release lock due to error", e);
} finally {
if (jedis != null) {
jedis.close();
}
}
log.info("release lock failed, requestToken:{}, result:{}", identify, result);
returnfalse;
}
思考:加锁和释放锁的原子性可以用lua脚本来保证,那锁的自动续期改如何实现呢?
redisson实现
redisson顾名思义,redis的儿子,本质上还是redis加锁,不过是对redis做了很多封装,它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
在引入redisson的依赖后,就可以直接进行调用。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
先来一段redisson的加锁代码
private void test() {
//分布式锁名 锁的粒度越细,性能越好
RLock lock = redissonClient.getLock("test_lock");
lock.lock();
try {
//具体业务......
} finally {
lock.unlock();
}
}
就是这么简单,使用方法jdk的ReentrantLock
差不多,并且也支持ReadWriteLock(读写锁)
、Reentrant Lock(可重入锁)
、Fair Lock(公平锁)
、RedLock(红锁)
等各种锁,详细可以参照redisson官方文档来查看。
- 那么redisson到底有哪些优势呢?
- 锁的自动续期(默认都是30秒),如果业务超长,运行期间会自动给锁续上新的30s,不用担心业务执行时间超长而锁被自动删掉。
- 加锁的业务只要运行完成,就不会给当前续期,即便不手动解锁,锁默认在30s后删除,不会造成死锁问题。
前面也提到了锁的自动续期,我们来看看redisson是如何来实现的。
先说明一下,这里主要讲的是redisson中的RLock
,也就是可重入锁,有两种实现方法。
// 最常见的使用方法
lock.lock();
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
而只有无参的方法是提供锁的自动续期操作的,内部使用的是“看门狗”机制,我们来看一看源码。
不管是空参还是带参方法,它们都调用的是同一个lock方法,未传参的话时间传了一个-1,而带参的方法传过去的就是实际传入的时间。
继续点进
scheduleExpirationRenewal
方法
点进
renewExpiration
方法
总结一下,就是当我们指定锁过期时间,那么锁到时间就会自动释放。如果没有指定锁过期时间,就使用看门狗的默认时间30s,只要占锁成功,就会启动一个定时任务,每隔10s给锁设置新的过期时间,时间为看门狗的默认时间,直到锁释放。
小结
虽然lock()有自动续锁机制,但是开发中还是推荐使用lock(time,timeUnit),因为它省掉了整个续期带来的性能损,可以设置过期时间长一点,搭配unlock(),若业务执行完成,会手动释放锁,若是业务执行超时,那一般我们服务也都会设置业务超时时间,就直接报错了,报错后就会通过设置的过期时间来释放锁。
public void test() {
RLock lock = redissonClient.getLock("test_lock");
lock.lock(30, TimeUnit.SECONDS);
try {
//.......具体业务
} finally {
//手动释放锁
lock.unlock();
}
}
这篇文章主要是redis实现分布式锁的方式,关于分布式锁实现方式还可以基于zookeeper、etcd等等,预计后续篇章会讲到,感兴趣的可以先关注支持一哈吖。
完!
都看到着了,点个赞点个关注再走呗~