本文的出现是为了能够分享个人所学的相关知识,检验自身学习成果。内容会和其他技术存在部分关联,如有任何描述错误或者说明有误的地方,还望各位大佬指出。
1. 背景
在分布式项系统的大背景下,CAP中分区容错性是必不可少的。多节点的协调尤为重要,单体服务中的锁在分布式系统中已无效,需要另辟蹊径。此文介绍比较常见的一种基于Redis实现的分布式锁。ZooKeeper的分布式锁后续Blog更新中介绍。
2. 环境
JDK:1.8
IDE:IDEA
Redis:Redis server v=5.0.5 bits=64 (redis配置此处不做赘述)
3.详解
Redis能实现分布式锁是因为存在一个命令:setnx ,而ex属性保证了一些容错。
3.1 使用LUA脚本
3.1.1 maven依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.1.2实现代码如下:
注:调用时value结果业务设定,过期时间也可以当参数传入。当业务的花费时间波动比较大,需要添加守护线程进行续命,确保业务不会因为锁的过期时间太短出现问题。
@Slf4j
@Component
public class RedisDistributedLock {
@Autowired
private RedisTemplate redisTemplate;
/**
* 过期时间10min
*/
@Value("${redis.expireTime:600000}")
private static long EXPIRE_TIMEOUT = 10 * 60 * 1000;
private static final Long SUCCESS = 1L;
/**
* 加锁
*
* @param key redis key
* @param value redis value
* @return 是否上锁成功
*/
public boolean lock(final String key, final String value) {
if (Strings.isBlank(key)) {
return false;
}
try {
String script = "if redis.call('setNx',KEYS[1],ARGV[1]) then if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end end";
RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);
Object result = redisTemplate.execute(redisScript, Collections.singletonList(key), value, String.valueOf(EXPIRE_TIMEOUT));
if (SUCCESS.equals(result)) {
return true;
}
} catch (Exception e) {
log.error("redis lock failed", e);
}
return false;
}
/**
* 解锁
*
* @param key redis key
* @param value redis value
* @return 是否解锁成功
*/
public boolean unlock(String key, String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class);
Object result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
if (SUCCESS.equals(result)) {
return true;
}
return false;
}
}
3.2 现有框架redisson
3.2.1 maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.3</version>
</dependency>
3.2.2 代码实现如下:
3.2.2.1 根据官方javadoc说明,使用redisson实现分布式锁的例子如下:
/**
* Acquires the lock only if it is free at the time of invocation.
*
* <p>Acquires the lock if it is available and returns immediately
* with the value {@code true}.
* If the lock is not available then this method will return
* immediately with the value {@code false}.
*
* <p>A typical usage idiom for this method would be:
* <pre> {@code
* Lock lock = ...;
* if (lock.tryLock()) {
* try {
* // manipulate protected state
* } finally {
* lock.unlock();
* }
* } else {
* // perform alternative actions
* }}</pre>
*
* This usage ensures that the lock is unlocked if it was acquired, and
* doesn't try to unlock if the lock was not acquired.
*
* @return {@code true} if the lock was acquired and
* {@code false} otherwise
*/
boolean tryLock();
3.2.2.2 此处提供基础的使用示例,redisson包含的很多类型的锁,包括公平锁,锁组,读写锁等,此处不逐一介绍。不难发现,redisson的锁和java提供的显式锁用法一致。
@Slf4j
@Component
public class RedisSonDistributedLock {
@Autowired
private Redisson redisson;
/**
* 过期时间10min
*/
@Value("${redis.expireTime:600000}")
private static long EXPIRE_TIMEOUT = 10 * 60 * 1000;
public void redissonDistributedLockDemo(final String key) {
RLock lock = redisson.getLock(key);
try {
boolean isLock = lock.tryLock(EXPIRE_TIMEOUT, TimeUnit.MILLISECONDS);
if (isLock) {
// TODO: 业务操作
}
} catch (InterruptedException e) {
log.error("tryLock failed.", e);
} finally {
lock.unlock();
}
}
}
此次介绍的分布式锁,只是一个简单的展示,具体的使用,需要根据业务处理。
比如Lua实现的分布式锁,当业务处理所需时间波动比较大时,预设的过期时间不好制定时,需要添加守护线程进行续命;同时也可以添加重试机制,确保加锁时不会因为其他非锁占用因素导致加锁失败。
本人自己也实现了分布式锁的starter,有兴趣可以看一下,starter源码地址。