本文已参加「新人创作礼」活动,一起开启创作之路。
前言
咱们知道,在Java单进程中,多线程的环境下,假如咱们要操作一个同享变量,需求运用synchronized或者是JUC同步工具类才干确保线程安全。那么,多进程环境下,咱们要怎样确保线程安全?
为什么需求分布式锁?
咱们知道,synchronized或者是JUC同步工具类只能在同一进程中确保线程安全,他们的影响范围没办法超出本Java进程。可是跟着分布式成为主流,多进程同享数据的状况越来越常见。
如上图,两个进程一起对存储在MySQL、Redis或者是zookeeper中的同享数据进行读写,即有或许呈现线程安全问题,这种状况下,咱们就需求一个能够在分布式环境下也能够运用的锁,来确保线程安全,这便是分布式锁。
分布式锁能够运用什么组件完成?
因为要在分布式环境下生效,因而完成分布式锁运用的组件也必须是每个进程都能够连接到的,现在比较常见的是运用Redis和Zookeeper来完成。
Redis分布式锁完成逻辑
Redis中,咱们运用String数据结构来完成分布式锁。
加锁
Redis中,set的语法如下:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
-
EX second
:设置键的过期时刻为second
秒。SET key value EX second
作用等同于SETEX key second value
。 -
PX millisecond
:设置键的过期时刻为millisecond
毫秒。SET key value PX millisecond
作用等同于PSETEX key millisecond value
。 -
NX
:只在键不存在时,才对键进行设置操作。SET key value NX
作用等同于SETNX key value
。 -
XX
:只在键已经存在时,才对键进行设置操作。
上述参数中,咱们能够将key
作为锁标识,然后设置NX
参数。假如key
写入成功,表明当前Redis中不存在这个key
,能够加锁;假如key
写入失利,表明当前Redis中已存在这个key
,已经有其它线程获取到锁了。指令如下:
SET lockKey requestId NX
lockKey
为锁标识,最好带上运用同享变量唯一标识,能够运用订单编号、用户编号等。
requestId
为本次锁恳求编号,开释锁时运用。
上述指令中,能够完成加锁,可是假如在加锁后,运用挂了,或者呈现了其它问题,导致没有及时解锁,就有或许呈现死锁,因而,需求再给加上一个过期时刻,让锁能够自动消失。指令如下:
SET lockKey requestId EX seconds NX
开释锁
开释锁的时候,咱们不能直接运用del
指令去删去Redis键值,不然会呈现A获取的锁,B也能够开释的状况。因而,咱们在开释锁的时候,需求判断当前锁的恳求ID是否是加锁时是否共同,假如共同,才干开释锁。
由于没有现成的指令能够完成上述的开释锁的逻辑,所以咱们需求运用Redis的script
来完成,script
脚本如下:
if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end
KEYS[1]
为锁标识,即加锁时的lockKey
ARGV[1]
为锁恳求编号,即加锁时的requestId
Redis分布式锁代码
public class RedisDistributedLock implements Lock {
// 锁键值
private String lockKey;
// 锁恳求编号
private String requestId;
// redis集群客户端
private JedisCluster jedisCluster;
public RedisDistributedLock(String lockKey, JedisCluster jedisCluster){
this.lockKey = lockKey;
// 运用UUID作为锁唯一标识
requestId = UUID.randomUUID().toString();
this.jedisCluster = jedisCluster;
}
/**
* 测验获取锁
*/
@Override
public boolean tryLock() {
// 获取锁
String result = jedisCluster.set(lockKey, requestId, "NX", "EX", 2);
return StringUtils.isNotBlank(result) && LOCK_SUCCESS.equals(result);
}
/**
* 开释锁
*/
@Override
public void unlock() {
// 若redis中存在lockKey,则删去lockKey,不然回来0
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedisCluster.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
// 成功
if (result != null && "OK".equals(result.toString())) {
logger.debug("开释锁成功,lockKey:{}, requestId:{}", lockKey, requestId);
} else {// 失利
logger.debug("开释锁失利,lockKey:{}, requestId:{}", lockKey, requestId);
}
}
}
后言
已然看到这里了,感觉有所收成的朋友,无妨来个大大的点赞吧~~~