我们先来看获取redis锁的set
命令:
SET resource_name random_value NX PX 30000
不用想得过于复杂,这里简单解释下上面这个命令:
SET resource_name random_value
直接可以理解为SET key value
,不过这里的key和value有它自己的含义。resource_name
是保存的业务名称,与上文数据库中使用的bussiness_code是同样的意思,这是区分不同业务的锁。random_value
则是随机的值,用于释放锁时候的校验。NX
:表示key不存在的时候设置成功,key存在的时候设置不成功。这相当于就是锁的操作,并且是一个原子性的操作。PX
:给Redis里面的key设置一个过期的时间,如果出现异常,锁可以过期失效。有上面的解释可以知道,我们主要是利用了NX的特性,多个线程并发的时候,只有一个线程设置成功,设置成功后即获得了锁,可以进行后续的业务处理。并且利用PX,如果过程中出现了异常,过了锁的有效期,锁就会自动释放。
当然有上锁,就会有释放锁。
在释放锁的时候会校验之前设置的redis的随机数,相同才能释放:这才能证明这个key的value是当前线程设置的。 这里释放锁,我们采用的是Lua脚本,因为redis的delete命令没有提供值校验的功能。
Lua脚本如下:
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
理解起来很简单,KEYS[1]
就是你的resource_name
,而ARGV[1]
就是程序传递过来的值,如果get到的这个值等于传递过来的值,那么就删除这个键值对。
lua脚本还具有以下优点:
- 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
- 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
- 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
再来说说,为什么释放锁的时候要去做随机数值的校验。
那么别的线程为什么能够获取到锁呢?是因为我们用了EX这个参数,设置了expire time。可以看看下图,如果不设置随机数校验会出现什么情况:
A获取到锁的时候,可能由于执行任务时间太长或者其他什么原因,锁过期了。而B获取到了锁,B在处理业务,那么此时如果A执行完了任务,去释放锁,就会释放到B现在持有到的锁。
GC 可能引发的安全问题
熟悉 Java 的同学肯定对 GC 不陌生,在 GC 的时候会发生 STW(Stop-The-World),这本身是为了保障垃圾回收器的正常执行。但是如果服务出现了 STW 且时间较长,就可能导致分布式锁进行了超时释放。
如果 Redis 采用单机部署模式,那就意味着当 Redis 故障了,就会导致整个服务不可用。
而如果采用主从模式部署,我们想象一个这样的场景:服务 A 申请到一把锁之后,如果作为主机的 Redis 宕机了,那么 服务 B 在申请锁的时候就会从从机那里获取到这把锁。
关于这个问题,可以参考Java架构直通车——RedLock是否可以做分布式锁。
我们新建项目,引入redis包:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
配置好properties,Controller如下。
@RestController
@Slf4j
public class RedisLockController {
@Autowired
RedisTemplate redisTemplate;
@RequestMapping("redisLock")
public String redisLock(){
log.info("我进入了方法");
String key="Biz_code";
String value= UUID.randomUUID().toString();
RedisCallback<Boolean> callback=new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
//ifAbsent()就是setNX
RedisStringCommands.SetOption setOption=RedisStringCommands.SetOption.ifAbsent();
//过期时间30秒
Expiration expiration=Expiration.seconds(30);
//序列化key,value
byte[] redis_key=redisTemplate.getKeySerializer().serialize(key);
byte[] redis_value=redisTemplate.getValueSerializer().serialize(value);
//执行setnx操作
Boolean result=redisConnection.set(redis_key,redis_value,expiration,setOption);
//返回操作结果
return result;
}
};
Boolean lock= (Boolean) redisTemplate.execute(callback);
if (lock) {
log.info("我进入了锁");
//模拟任务执行15秒
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//执行lua脚本,释放锁
String script="if redis.call(\"get\",KEYS[1])==ARGV[1] then\n" +
"\treturn redis.call(\"del\",KEYS[1])\n" +
"else\n" +
"\treturn 0\n" +
"end";
RedisScript<Boolean> redisScript=RedisScript.of(script,Boolean.class);
List keys= Arrays.asList(key);
Boolean unlock= (Boolean) redisTemplate.execute(redisScript,keys,value);
log.info("释放锁的结果:"+unlock);
}
}
log.info("方法执行完成");
return "方法执行完成";
}
}
我们首先启动一个8080端口,运行结果如下:
结果没有问题,再启动一个8081端口,同时打两个请求。我们只查看后面一个没有获取到锁的结果:
Redisson提供了现成的API来实现分布式锁,可以参考:github redisson quickstart
// 4. Get Redis based Lock
RLock lock = redisson.getLock("myLock");
RLockReactive lockReactive = redissonReactive.getLock("myLock");
RLockRx lockRx = redissonRx.getLock("myLock");
在此不做赘述。
对比一下Redisson和我们自己实现的RedisLock,RedisLock没有支持阻塞。