当我们的系统在一起的时候,假如去操作共享的数据的时候,我们可以加锁来保证数据的安全。但是当我们系统被拆分成多个服务,或者不同的系统去共享一部分数据,而此数据可以被多个系统修改的时候,比如多个系统去修改一个订单的信息。为了保证数据的可靠,我们需要在分布式或者多系统之间找出一种锁的实现方式。
而在分布式系统中实现锁的途径有多种。这里我们先讲Reids的方式。
首先要声明的本例子使用的版本信息
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.0.8.RELEASEversion>
<relativePath/>
parent>
另外本例子只是一个很简单的实现,并没有实现锁的重入。
我们要实现一个锁,一般来说要保证以下要求;
上面的要求就需要我们保证代码在设置并且判断是否有值的时候需要原子操作,以及锁需要有个时间、保证锁最终可以成功释放掉。
假设两个系统都是订单处理系统,现在都去订单系统中拉取订单进行处理。这个时候可能存在APP1和APP2都拉取一个订单进行处理。这样就可能出现重复处理。
那么我们现在在订单之前加一个获取锁的操作
此时我们APP1 优先请求订单1的锁,此时APP2再去申请订单1的锁会被拒绝。这样就避免重复操作。
ps.关于第三条,存在一个极端情况
redisTemplate.opsForValue().setIfAbsent
方法虽然内部使用了connection.setNX
的方法,虽然保证设置参数的原子性,但是在之前版本中设置超时时间的时候却需要额外操作。为了保证原子化的操作,所以我们将判断和参数设置写到lua脚本中,通过调用过程执行来保证原子操作。
编写lua脚本
/**
* 拿锁的脚本
*/
private static final String LUA_SCRIPT_LOCK = "return sample.redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) ";
/**
* 拿锁的脚本
*/
private static RedisScript<String> scriptLock = new DefaultRedisScript<>(LUA_SCRIPT_LOCK, String.class);
/**
* 释放锁锁的脚本
*/
public static final String LUA_SCRIPT_UN_LOCK = "if " +
"(sample.redis.call('GET', KEYS[1]) == ARGV[1] )" +
"then " +
"return sample.redis.call('DEL', KEYS[1]) " +
"else " +
"return 0 " +
"end";
/**
* 释放锁的脚本
*/
private static RedisScript<Long> scriptLock2 = new DefaultRedisScript<>(LUA_SCRIPT_UN_LOCK, Long.class);
获得锁
public RedisLock getLock(String key,long timeOut,long tryLockTimeout) {
long timestamp = System.currentTimeMillis();
try {
key = getKey(key);
UUID uuid = UUID.randomUUID();
while ((System.currentTimeMillis() - timestamp) < tryLockTimeout) {
// 参数分别对应了脚本、key序列化工具、value序列化工具,后面参数对应scriptLock字符串中的三个变量值,
// KEYS[1],ARGV[1],ARGV[2],含义为锁的key,key对应的value,以及key 的存在时间(单位毫秒)
String result = (String) redisTemplate.execute(scriptLock,
redisTemplate.getStringSerializer(),
redisTemplate.getStringSerializer(),
Collections.singletonList(key),
uuid.toString(),
String.valueOf(timeOut));
if (result != null && result.equals("OK")) {
return new RedisLock(key, uuid.toString());
} else {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
log.error(JSON.toJSONString(e.getStackTrace()));
}
return null;
}
释放锁
/**
* 释放锁
* @param lock
*/
public void releaseLock(RedisLock lock) {
Object execute = redisTemplate.execute(scriptLock2,
redisTemplate.getStringSerializer(),
redisTemplate.getStringSerializer(),
Collections.singletonList(lock.getKey()),
lock.getValue()
);
// 当返回0的时候可能因为超时而锁已经过期
if (new Integer("1").equals(execute)) {
log.info("释放锁");
}
}
锁对象
@Data
public class RedisLock {
/**
* 锁的key
*/
private String key;
/**
* 锁的值
*/
private String value;
public RedisLock(String key, String value) {
this.key = key;
this.value = value;
}
}
因为我们只是一个很简单的实现,没有实现锁重入,所以只是简单的一个线程测试了,锁的效果
多个应用请求一个锁,此时第二个请求无法获得锁
public void testLock() {
redisTemplate.delete("redisLock");
redisTemplate.delete("redisLock2");
RedisLock redisLock1 = manager.getLock("redisLock", 10000L, 5000L);
if (redisLock1 == null) {
log.info("未获得锁");
} else {
log.info("获得锁");
}
Assert.assertNotNull(redisLock1);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 因为非重入锁,所以此时再获取锁失败
RedisLock redisLock2 = manager.getLock("redisLock", 10000L, 5000L);
Assert.assertNull(redisLock2);
if (redisLock2 == null) {
log.info("未获得锁");
} else {
log.info("获得锁");
}
}
获得超时锁
此时因为上一个请求锁到期,后面的请求也可以获得锁。
@Test
public void testLock2() {
redisTemplate.delete("redisLock");
redisTemplate.delete("redisLock2");
RedisLock redisLock3 = manager.getLock("redisLock2", 10000L, 5000L);
if (redisLock3 == null) {
log.info("未获得锁");
} else {
log.info("获得锁");
}
Assert.assertNotNull(redisLock3);
try {
Thread.sleep(15000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 因为非重入锁,所以此时再获取锁失败
RedisLock redisLock4 = manager.getLock("redisLock2", 10000L, 5000L);
Assert.assertNotNull(redisLock4);
if (redisLock4 == null) {
log.info("未获得锁");
} else {
log.info("获得锁");
}
}
正常的释放和获得锁
@Test
public void testLock3() {
redisTemplate.delete("redisLock");
redisTemplate.delete("redisLock2");
redisTemplate.delete("redisLock3");
RedisLock redisLock3 = manager.getLock("redisLock3", 10000L, 5000L);
if (redisLock3 == null) {
log.info("未获得锁");
} else {
log.info("获得锁");
}
Assert.assertNotNull(redisLock3);
manager.releaseLock(redisLock3);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 因为非重入锁,所以此时再获取锁失败
RedisLock redisLock4 = manager.getLock("redisLock2", 10000L, 5000L);
Assert.assertNotNull(redisLock4);
if (redisLock4 == null) {
log.info("未获得锁");
} else {
log.info("获得锁");
}
}
在2.1版本之后spring-data-redis丰富了setIfAbsent
方法。
[外链图片转存失败(img-JzsGKNyq-1565530962149)(DEDE24A8084749A19F83DF73021C0BB9)]
要使用这个方法,之前我尝试将spring-boot-starter-data-redis
修改为最新的2.1.7后来发现实际引入的spring-data-redis
还是2.0.13所以我只能修改我们的依赖。
<dependencies>
<dependency>
<artifactId>base-coreartifactId>
<groupId>daifyutilsgroupId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<version>2.1.6.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-redisartifactId>
<version>2.1.6.RELEASEversion>
dependency>
<dependency>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
<version>5.1.6.RELEASEversion>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.6.2version>
dependency>
dependencies>
project>
加锁
我们可以使用这个方法完成加锁
public RedisLock getLock(String key,long timeOut,long tryLockTimeout) {
long timestamp = System.currentTimeMillis();
try {
key = getKey(key);
UUID uuid = UUID.randomUUID();
while ((System.currentTimeMillis() - timestamp) < tryLockTimeout) {
Boolean aBoolean =
redisTemplate.opsForValue().setIfAbsent(key, uuid.toString(), timeOut, TimeUnit.MILLISECONDS);
if (aBoolean) {
return new RedisLock(key, uuid.toString());
} else {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
log.error(JSON.toJSONString(e.getStackTrace()));
}
return null;
}
当然这种方法目前我保留意见,因为需要我们额外修改依赖版本,会不会出现未知的问题,暂时没法确定。
之前写代码的时候出现过Redis exception; nested exception is io.lettuce.core.RedisException: java.lang.IllegalStateException
的错误。
org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is io.lettuce.core.RedisException: java.lang.IllegalStateException
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:74)
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:257)
at org.springframework.data.redis.connection.lettuce.LettuceScriptingCommands.convertLettuceAccessException(LettuceScriptingCommands.java:236)
at org.springframework.data.redis.connection.lettuce.LettuceScriptingCommands.evalSha(LettuceScriptingCommands.java:195)
at org.springframework.data.redis.connection.DefaultedRedisConnection.evalSha(DefaultedRedisConnection.java:1240)
当时是因为我设置了Integer.class
的返回类型
private static RedisScript<Integer> scriptLock2 = new DefaultRedisScript<>(LUA_SCRIPT_UN_LOCK,Integer.class);
后来发现支持的返回类型判断,并不存在Integer
if (javaType == null) {
return ReturnType.STATUS;
}
if (javaType.isAssignableFrom(List.class)) {
return ReturnType.MULTI;
}
if (javaType.isAssignableFrom(Boolean.class)) {
return ReturnType.BOOLEAN;
}
if (javaType.isAssignableFrom(Long.class)) {
return ReturnType.INTEGER;
}
return ReturnType.VALUE;
本篇文章涉及的源码下载地址:https://gitee.com/daifyutils/springboot-samples