1、前言
在《基于MySQL数据库排它锁(for update)实现的分布式锁》中,我们学习了基于Mysql实现的分布式锁,这里我们开始学习基于Redis实现的分布式锁。在这里,我们将尝试两种方法,一种是基于Redis的RedisTemplate客户端实现,一种是基于Redisson实现。
2、基于Redis实现的分布式锁
2.1、实现原理
主要是利用Redis的setnx命令实现分布式锁。一般使用如下命令:
set resource_name my_random_value NX PX 30000
其中,
- resource_name 资源名称,可以根据不同的业务区分不同的锁
- my_random_value 随机值,用来作为释放锁时,做校验,避免释放掉其他线程的锁,所以该随机数需要不同线程产生不同的随机数,一般使用UUID。
- NX key不存在时设置成功,key存在时则设置不成功
- PX 自动失效时间,出现异常情况,锁可以过期失效
使用该命令实现分布式锁,主要利用了NX的原子性,多线程并发时,只有一个线程可以设置成功,获取到锁后,可以执行后续的业务逻辑(如果出现异常,等待锁过期失效),执行完业务逻辑后,通过Redis的delete命令释放锁,释放锁时需要校验随机数(避免出现释放掉其他线程的锁),为了保证释放锁的原子性,一般采用Lua脚本实现。
2.2、搭建环境
这里主要基于Spring Boot、redis等组件搭建项目。该项目作为一个模块,继承自qriver-distributed-lock,其中pom文件定义如下:
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.qriver</groupId>
<artifactId>qriver-distributed-lock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.qriver.distributedlock</groupId>
<artifactId>distributedlock-redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.properties文件配置如下:
server.port=8080
#redis配置
spring.redis.host=127.0.0.1
spring.redis.database=0
spring.redis.port=6379
spring.redis.password=123456
#日志配置
logging.level.com.qriver.distributedlock=debug
最后,SpringBoot默认的启动类,如下:
@SpringBootApplication
public class DistributedlockRedisApplication {
public static void main(String[] args) {
SpringApplication.run(DistributedlockRedisApplication.class, args);
}
}
2.3、实现基于Redis的分布式锁
2.3.1、RedisConfig配置类
实现RedisConfig配置类,主要是注入RedisTemplate对象。
/**
* Redis 基础配置
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
//key序列化
RedisSerializer keySerializer = new StringRedisSerializer();
RedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setKeySerializer(keySerializer);
//value序列化
redisTemplate.setValueSerializer(valueSerializer);
//hash key 序列化
redisTemplate.setHashKeySerializer(keySerializer);
//hash value 序列化
redisTemplate.setHashValueSerializer(valueSerializer);
//redis初始化
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisTemplate<String, Object> redisTemplateGroup(RedisConnectionFactory factory) {
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
RedisSerializer keySerializer = new StringRedisSerializer();
RedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(getMapper());
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setValueSerializer(valueSerializer);
redisTemplate.setHashKeySerializer(keySerializer);
redisTemplate.setHashValueSerializer(valueSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 获取JSON工具
* @return
*/
private final ObjectMapper getMapper() {
ObjectMapper mapper = new ObjectMapper();
//将类名称序列化到json串中,去掉会导致得出来的的是LinkedHashMap对象,直接转换实体对象会失败
//设置输入时忽略JSON字符串中存在而Java对象实际没有的属性
//其中该配置,需要升级fasterxml版本
//mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
}
2.3.2、RedisLock 分布式锁
首先,RedisLock 分布式锁实现了AutoCloseable 接口,通过实现close方法,使得发生异常或逻辑执行完成后,自动释放分布式锁。
AutoCloseable接口位于java.lang包下,从JDK1.7开始引入。引入该接口就是为了更好的进行管理资源,准确说是资源的释放。当一个类实现了该接口close方法,在使用try-catch-resources语法创建的资源抛出异常后,JVM会自动调用close 方法进行资源释放,当没有抛出异常正常退出try-block时候也会调用close方法。像数据库链接类Connection,io类InputStream或OutputStream都直接或者间接实现了该接口。
然后,提供了一个构造函数,在需要使用分布式锁的地方,需要通过new方法创建锁对象。
又提供了加锁方法getLock(),其中通过redisTemplate.execute()方法执行Redis命令,实际上就是通过setnx命令为一个指定的key设置value值,如果设置成功就表示获取到锁了(在没有释放锁或锁失效前,其他线程将无法获取到锁),否则就是获取锁失败。
最后,提供了一个释放锁的方法unLock(),该方法是通过执行了一一段LUA脚本实现了锁释放(比较操作和删除操作)的原子性。
public class RedisLock implements AutoCloseable {
private Logger logger = LoggerFactory.getLogger(RedisLock.class);
private String key;
private String value;
//单位:秒
private int expireTime;
private RedisTemplate<String, Object> redisTemplate;
public RedisLock(RedisTemplate<String, Object> redisTemplate, String key,int expireTime){
this.redisTemplate = redisTemplate;
this.key = key;
this.expireTime = expireTime;
this.value = UUID.randomUUID().toString();
}
/**
* 获取分布式锁
* @return
*/
public boolean getLock(){
logger.debug("进入获取锁的方法。");
RedisCallback<Boolean> redisCallback = connection -> {
//设置NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
//设置过期时间
Expiration expiration = Expiration.seconds(expireTime);
//序列化key
RedisSerializer keySerializer = new StringRedisSerializer();
byte[] redisKey = keySerializer.serialize(key);
//序列化value
RedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
byte[] redisValue = valueSerializer.serialize(value);
//执行setnx操作
Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
return result;
};
//获取分布式锁
Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
logger.debug("获取分布式锁的结果:" + lock);
return lock;
}
public boolean unLock() {
logger.debug("进入释放锁的方法。");
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
List<String> keys = Arrays.asList(key);
Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
logger.debug("释放锁的结果:"+result);
return result;
}
@Override
public void close() throws Exception {
unLock();
}
}
2.3.3、RedisLock 分布式锁的应用
这里实现一个DemoController类,来应用基于Redis实现的分布式锁,实现如下:
@RestController
public class DemoController {
private Logger logger = LoggerFactory.getLogger(DemoController.class);
@Resource
private RedisTemplate<String, Object> redisTemplate;
@RequestMapping("redisLock")
@Transactional(rollbackFor = Exception.class)
public String testLock() throws Exception {
logger.debug("进入testLock()方法;");
try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){
if (redisLock.getLock()) {
logger.debug("获取到分布式锁;");
//注意,如果大于超时时间,则会释放锁失败
Thread.sleep(20 * 1000);
}
} catch (Exception e) {
e.printStackTrace();
}
logger.debug("方法执行完成;");
return "方法执行完成";
}
}
2.3.4、测试
分别启动端口为8080、8081的两个实例,启动方法可以参考《IntelliJ Idea如何为一个项目启动多个项目实例》。然后,在浏览器分别访问http://localhost:8081/redisLock、 http://localhost:8080/redisLock两个地址,在控制台打印的日志如下:
通过打印日志的时间我们可以知道,和基于数据库的一个很重要的区别就是:在基于数据库的实现中,线程会等待获取锁,然后执行业务逻辑,而在基于Redis的实现中,则直接返回获取锁失败,并执行获取锁失败的后续逻辑,即不支持阻塞。