- 什么是分布式锁
1.1 作用:
保证数据的正确性: 比如:秒杀的时候防止商品超卖,接口幂等性。
避免重复处理数据: 比如:1避免调度任务在多台机器重复执行,2避免缓存过期所有请求都去加载数据库。
一个分布式锁需要考虑的问题:
1互斥阻塞。2锁需要可重入。3过期时间 4锁续期
1.2 redission的实现原理是什么?
1.2.1 如何解决这四个问题呢?
redission如何解决互斥 : redis内部使用key冲突,解决互斥,也就是相同的key只能出现一次。 使用lua脚本进行加锁和设置expire,保证原子。
可重入:每个线程在进行获取锁前都会去生成一个唯一id, 会判断加锁是否为自己的UUID。来进行可重入判断。
锁
过期时间: 通过expire进行过期过期时间。
锁续期: 看门狗机制进行锁时间的续期,默认锁时间设置的是30S,到10S,三分之一的时候会把锁时间重新设置为30S。
取消更新锁时长的任务场景有几种 : - 1正常释放锁
- 2关闭了Redisson实例
- 客户端在持有锁的过程中崩溃,也就是服务器down机,看门狗也会因为无法继续运行而停止续约,这样,过了租约时间后,锁就会被自动释放。
1.2.2 redission使用的什么数据结构。
锁结构举例,redission使用hash结构进行加锁的。
{
“lock-name”: {
//线程id
“threadId”: “Java-VM-thread-ID”,
//UUID , 用于判断可重入,当设置锁时生成UUID。
“UUID”: “unique-identifier”,
//看门狗守护线程,进行续约。
“lockWatchdogTimeout”: “timeout-in-seconds”,
//加锁时间
“internalLockLeaseTime”: “lease-time-in-seconds”
}
}
源码:
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//加锁成功的话,创建hashset,并设置过期时间
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果不存在进行判断线程是否为自己。
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
redis在设置leaseTime的情况下,看门狗是不会续命的。
当不使用leaseTime的情况下,会设置为-1.
1比如我加锁时间是60S, 然后业务执行了70S,在60S的时候,锁会释放, 也就出现了重复加锁的情况, 所以需要根据业务设置合理的加锁时长。
2当我没有设置leaseTime时,默认为-1,也就是永远不停。 为了使用同一套实现 也就是 setnx expire同时使用, 那么默认会设置一个时长为30S,的key。 然后进行锁续命,续命来解决锁失效的问题。
代码中看门狗也就是一个定时任务,会在1/3的时候进行更新任务。
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName() + " expiration", future.cause());
return;
}
if (future.getNow()) {
// reschedule itself
scheduleExpirationRenewal(threadId);
}
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
internalLockLeaseTime默认为30S。
为什么redis不设置一个永久不过期的时间,而是使用续期的方式进行保证永不过呢?
我的理解是redis想要复用同一套代码,也就是底层全部使用这一套lua。所以使用上层定时维护key时长的方式,达到key永不过期的效果。
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//加锁成功的话,创建hashset,并设置过期时间
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
实战:
场景分类:
1单个锁资源,资源可从前端传入。(简单场景) ----使用注解加锁。
2单个锁,复杂场景 使用模版进行处理。 ---- 自定义,使用模版加锁。
3批量场景,一般资源都是后端查出相关的进行锁定,这种复杂的,—自定义处理,使用模版加锁。
1.2.2.1 分布式锁注解
package com.pzhu.spring.cloud.alibaba.consumer.annotation;
import com.pzhu.spring.cloud.alibaba.consumer.Enum.RedisLockEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
-
分布式锁注解
-
@author jinhaiyang
*/
@Retention(RetentionPolicy.RUNTIME)//运行时生效
@Target(ElementType.METHOD)//作用在方法上
public @interface RedissonLock {/**
- key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做分布式锁,就自己指定
- @return key的前缀
*/
String prefixKey() default “”;
/**
- springEl 表达式
- @return 表达式
*/
String key();
/**
- 等待锁的时间,默认-1,不等待直接失败,redisson默认也是-1
- @return 单位秒
*/
int waitTime() default -1;
/**
- 自动释放锁时长,默认为-1,也就是不会自动释放锁。根据业务设置自动释放锁时长,避免死锁
- @return 单位秒
*/
int expireTime() default -1;
/**
- 等待锁的时间单位,默认毫秒
- @return 单位
*/
TimeUnit unit() default TimeUnit.MILLISECONDS;
}
1.2.2.2 使用说明:
含义
默认值
是否必填
prefixKey
锁前缀
方法的全命名
否
key
操作锁定key
无
是
waitTime
默认锁等待
-1
否
expireTime
释放锁时间
-1
否
unit
锁时间单位
毫秒
否
1.2.2.2.1 单个资源使用示例
1.2.2.2.1.1 使用自定义注解,锁资源可以通过前端传入。只能单个资源加锁。
我用reqDto中的resumeId作为加锁的key,加锁的前缀为reception-web:listLock 因为没有指定默认等待时间,所以立即获取到锁,获取不到会自动失败。因为没有指定expireTime,默认为-1,所以并不会自动释放锁。
@PostMapping(“list”)
@RedissonLock(prefixKey = “reception-web:listLock” ,key = “#order.id”)
public List list(@RequestBody Order order ) {
System.out.println(JSON.toJSONString(reqDTO));
return serviceFeign.list();
}
1.2.2.2.1.2 自己有复杂业务处理的,使用以下模版。
if (Boolean.TRUE.equals(redissionUtil.tryAcquire(RedisLockEnum.ROOM_ARRANGE,GatewayHeaderUtil.getHotelCode() + RedissionUtil.LOCK_ARRANGE))) {
try {
//加锁成功后业务处理
rowHouses(orderArrangeReq);
return true;
} catch (Exception e) {
// 加锁异常后自定义处理
log.error(“rowHouses hotelCode为{} 排房期间出现未知异常”, GatewayHeaderUtil.getHotelCode(), e);
throw e;
} finally {
//重要! 必须在finally中解锁。
redissionUtil.release(RedisLockEnum.ROOM_ARRANGE,GatewayHeaderUtil.getHotelCode() + RedissionUtil.LOCK_ARRANGE);
}
} else {
throw new ReceptionException(ReceptionEnums.ROOM_ARRANGE_NOW, ReceptionEnums.ROOM_ARRANGE_NOW.getResultMsg());
}
1.2.2.2.2 多资源加锁-批量加锁
一般不存在这种场景, 比如当我要去通过团队id,查询团队下的订单id,锁住这些订单。我需要在业务中进行查询,这种情况比较复杂,所以不建议使用注解实现。
使用模版
if (Boolean.TRUE.equals(redissionUtil.multiLockTryAcquire(roomNos,GatewayHeaderUtil.getHotelCode(), 30L))) {
try {
rowHouses(orderArrangeReq);
return true;文章来源:https://www.toymoban.com/news/detail-583258.html
} catch (Exception e) {
log.error(“rowHouses hotelCode为{} 房间号列表为{} 排房期间出现未知异常”,GatewayHeaderUtil.getHotelCode(), JSON.toJSONString(roomNos), e);
throw e;
} finally {
redissionUtil.mutiLockRelease(roomNos,GatewayHeaderUtil.getHotelCode());
}
} else {
//加锁失败,证明存在部分房间冲突
throw new ReceptionException(ReceptionEnums.ROOM_IS_ARRANGE_NOW, ReceptionEnums.ROOM_IS_ARRANGE_NOW.getResultMsg());
}
参考文章:
面试官竟然问我怎么实现分布式锁?幸亏我总结了全套八股文
Redis 分布式锁的正确实现原理演化历程与 Redission 实战总结文章来源地址https://www.toymoban.com/news/detail-583258.html
到了这里,关于分布式锁,学习笔记的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!