目前越来越多的应用使用负载均衡,以往传统单体应用单机部署的情况下使用的JAVA并发处理资源竞争方式(J.U.C或synchronized等)在集群部署中已经无法保证资源的安全访问。
需要考虑以下情况——
为了解决分布式应用中对资源的安全访问于是便有了分布式锁。
实现分布式锁一般基于Zookeeper或者Redis,前者可靠性高而后者效率高。
如果并发量不大追求可靠性选择Zookeeper,反之选择Redis。
分布式锁一大特点就是排他性,临界条件下仅有获得锁的调用者才能访问资源。
而Redis是基于内存
设计K-V数据库且单线程
执行命令。
因此使用Redis作为分布式锁的中间件满足两个重要条件——
而且 Redis支持集群部署(sentinel, cluster)保障了可用性。
使用SETNX()便可以完成加锁操作。SETNX表示如果Key不存在则写入,如果存在则什么都不做。
setnx key value
这样便只允许一个调用者可以访问。等等似乎少了什么?没错我们没有设置KEY的过期时间,如果此时调用者宕机这把锁就无法释放了。
我们使用EXPIRE加上过期时间,默认单位为秒。
EXPIRE key seconds
但是这样做就完了吗?SETNX跟EXPIRE是两个独立的操作,即加锁操作不具备原子性。假如加锁调用成功,但是设置过期时间的时候调用服务宕机,依然存在锁无法正常释放的问题。
于是我们还需要把这两条命令合为一条命令。
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
目前终于算是完成了原子加锁命令并且锁存在过期时间即使调用者宕机到期之后也会自动释放。
使用DEL便可以删除锁。
DEL key [key ...]
不过事情真的有这么简单吗?当然不会(套路啊)
别绕晕了画张图更直观的感受下——
从图中可以发现A、B有并行,B、C有并行不符合我们的要求。
那么怎样才能让调用者只能释放自己占用的锁呢?这个时候K-V中的V就发挥作用了。如果我们的Value都是唯一(例如:UUID)的并且调用者释放锁需要验证Value便可以避免释放其他调用者占用的锁资源。
SET key uuid EX times NX
那么在释放锁的时候就跟需要分三步——
可是这三个步骤是独立非原子操作,如果在2、3步骤之间锁因为超时自动释放且在高并发情况下别其他调用者加锁成功,在执行步骤3时则仍然存在删除其他调用者的锁。
因此我们仍然需要原子操作。遗憾的是Redis的命令并不提供该操作。
别慌Redis虽然没有提供,但是Redis支持LUA脚本,LUA脚本可以帮助原子性的完成这一系列复合操作。
以下为Redis官方提供脚本——
if redis.call('get', KEYS[1]) == ARGV[1]
then
return redis.call('del', KEYS[1])
else
return 0
end
这个脚本即使你不会LUA大概也猜得到是什么意思吧。
执行脚本如下——
eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock value
现在释放锁的原子操作也满足了,让我们看看代码实践。
笔者这里使用springboot来演示分布式锁。
因为分布式锁的实现可以是多种方式建议抽象一个接口,例如DistributedLock。
如果使用Redis作为分布式锁中间件则创建RedisDistributedLock。
如果使用Zookeeper作为分布式锁的中间件则创建ZookeeperDistributedLock。
可以根据@ConditionalOnBean以及spring优先级选择加载那种分布式锁。
分布式锁配置
spring:
redis:
lettuce:
pool:
max-active: 100
max-idle: 50
min-idle: 30
max-wait: 2000ms
# 哨兵模式
sentinel:
master: mymaster
nodes: ${ARGS_REDIS_NODES}
# 集群模式
# cluster:
# nodes: ${ARGS_REDIS_NODES}
password: ${ARGS_REDIS_PASSWORD}
# 分布式配置
distributed:
lock:
expire: 30000
# connection-timeout-ms: 15000
# session-timeout-ms: 60000
分布式锁接口
public interface DistributedLock {
/**
* 获取锁
*
* @param key
* @param value
* @return
*/
public boolean tryLock(String key, String value);
/**
* 释放锁
*
* @param key
* @param value
* @return
*/
public boolean releaseLock(String key, String value);
}
分布式锁实现
/**
* 分布式锁:使用Redis实现
*/
@AutoConfigureAfter({RedisAutoConfiguration.class})
@ConditionalOnBean(RedisAutoConfiguration.class)
@Service
@Slf4j
public class RedisDistributedLock implements DistributedLock {
private final static String OK = "OK";
private static final String UNLOCK_LUA;
/**
* Lua脚本来释放锁
*/
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('get', KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call('del', KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end");
UNLOCK_LUA = sb.toString();
}
/**
* 缺省超时时间
*/
@Value("${distributed.lock.expire}")
private long expire;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean tryLock(String key, String value) {
return tryLock(key, value, TimeUnit.MILLISECONDS, expire);
}
@Override
public boolean releaseLock(String key, String value) {
try {
return stringRedisTemplate.execute(
(RedisCallback<Boolean>) connection -> connection.eval(
UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN, 1,
key.getBytes(Charset.forName("UTF-8")),
value.getBytes(Charset.forName("UTF-8"))));
} catch (Exception ex) {
log.error("释放redis锁失败", ex);
}
return false;
}
/**
* 获取锁
*
* @param key
* @param value
* @param timeUnit
* @param time
* @return
*/
private boolean tryLock(String key, String value,
TimeUnit timeUnit, long time) {
try {
return stringRedisTemplate.execute(
(RedisCallback<Boolean>) connection -> connection.set(
key.getBytes(Charset.forName("UTF-8")),
value.getBytes(Charset.forName("UTF-8")),
Expiration.milliseconds(timeUnit.toMillis(time)),
RedisStringCommands.SetOption.SET_IF_ABSENT)
);
} catch (Exception ex) {
log.error("获取redis锁失败", ex);
}
return false;
}
}
调用示例
boolean lock = distributedLock.tryLock(LOCK_NAME, lockValue);
try {
if (!lock) {
log.info("已存在分布式锁:[{}]", LOCK_NAME);
return;
}
} finally {
if (lock) {
distributedLock.releaseLock(LOCK_NAME, lockValue);
}
}
以上就是对分布式锁的代码演示。
Redis实现分布式锁还有哪些问题呢?
自动续期
当业务逻辑处理时间超过锁的过期时间需要有监控线程在过期之前进续期。你可以将其称之为Monitor或者Watchdog。
可重入
分布式锁能够支持一个线程对资源的重复加锁吗?
读写锁
所谓读写锁即:读读不互斥,读写互斥,写写互斥。例如:ReentrantReadWriteLock。能否想使用J.U.C包下面的类一样使用分布式锁呢?
集群
Redis集群环境是一个复杂的逻辑结构,节点的上线下线、主从复制、从节点的提升导致的数据丢失考虑了吗?
哈哈,是不是感觉头都大了。
好在Redis官方给我们指了一条明路。这就是接下来笔者要介绍的内容了。
Redisson正式笔者要介绍的Redis分布式锁终极解决方案。什么你没听说过?没关系跟着笔者进入新世界的大门。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(
BitSet
,Set
,Multimap
,SortedSet
,Map
,List
,Queue
,BlockingQueue
,Deque
,BlockingDeque
,Semaphore
,Lock
,AtomicLong
,CountDownLatch
,Publish / Subscribe
,Bloom filter
,Remote service
,Spring cache
,Executor service
,Live Object service
,Scheduler service
) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
关于Redisson项目的详细介绍可以在官方网站找到。每个Redis服务实例都能管理多达1TB的内存。
能够完美的在云计算环境里使用,并且支持AWS ElastiCache主备版,AWS ElastiCache集群版,Azure Redis Cache和阿里云(Aliyun)的云数据库Redis版
一言以蔽之可以像使用本地J.U.C一样使用Redis分布式锁。
新增依赖支持
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.13.2version>
dependency>
YAML文件配置
redisson:
client:
sentinel-address:
- redis://${redis.senntinel.node1}
- redis://${redis.senntinel.node2}
- redis://${redis.senntinel.node3}
master-name: mymaster
database: 0
password: ${redis.password}
新增RedissonConfig配置类
@Configuration
@ConfigurationProperties(prefix = "redisson.client")
@Getter
@Setter
public class RedissonConfig {
private String masterName;
private int database;
private String password;
private String[] sentinelAddress;
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSentinelServers()
.setMasterName(masterName)
.setPassword(password)
.setDatabase(database)
.setCheckSentinelsList(false)
.addSentinelAddress(sentinelAddress);
return Redisson.create(config);
}
}
测试方法
@GetMapping(value = "/api/lock")
public String testRedisson() throws InterruptedException {
// 锁对象
RLock rLock = redissonClient.getLock("lock");
// rLock.tryLock();
// 阻塞式获取锁
rLock.lock();
try {
log.info("the lock for {}-{}", Thread.currentThread().getName(), Thread.currentThread().getId());
// 超过默认的30秒锁过期时间
Thread.sleep(1000 * 120);
} finally {
rLock.unlock();
log.info("release the lock for {}-{}", Thread.currentThread().getName(),
Thread.currentThread().getId());
}
return "";
}
验证结果如下所示——
由此可以Redisson帮我们自动续期。抓取一下Redis的请求信息看看——
"EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:97"
"EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
观察代码发现使用LUA脚本续期,Redisson中大量使用LUA脚本保证其命令具有原子性。
1594739476.909029 [0 101.88.95.75:25242] "EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:97"
1594739476.909135 [0 lua] "exists" "lock"
1594739476.909158 [0 lua] "hincrby" "lock" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:97" "1"
1594739476.909176 [0 lua] "pexpire" "lock" "30000"
1594739476.909201 [0 101.88.95.75:25242] "WAIT" "2" "1000"
1594739477.166176 [0 10.10.10.101:36452] "PING"
1594739477.452893 [0 10.10.10.101:36468] "PING"
1594739477.619219 [0 10.10.10.101:36468] "PUBLISH" "__sentinel__:hello" "172.17.0.3,26380,2d8a2463c5242b26b0fc58a07b444ed28fca45c6,0,mymaster,10.10.10.101,6379,0"
1594739477.766886 [0 10.10.10.101:36484] "PING"
1594739477.870853 [0 10.10.10.101:36484] "PUBLISH" "__sentinel__:hello" "172.17.0.3,26381,037ccf65d8178a1aaac14bbb40fc28b2cebd4eac,0,mymaster,10.10.10.101,6379,0"
1594739478.017627 [0 101.88.95.75:25247] "EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
1594739478.017727 [0 lua] "exists" "lock"
1594739478.017738 [0 lua] "hexists" "lock" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
1594739478.017747 [0 lua] "pttl" "lock"
1594739478.017758 [0 101.88.95.75:25247] "WAIT" "2" "1000"
1594739478.047182 [0 101.88.95.75:25263] "SUBSCRIBE" "redisson_lock__channel:{lock}"
1594739478.065068 [0 101.88.95.75:25251] "EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
1594739478.065226 [0 lua] "exists" "lock"
1594739478.065241 [0 lua] "hexists" "lock" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
1594739478.065256 [0 lua] "pttl" "lock"
1594739478.065270 [0 101.88.95.75:25251] "WAIT" "2" "1000"
MONITOR日志中也证实了Redisson操作使用了大量的LUA脚本。
Redisson的锁集成了J.U.C
下的Lock
接口,提供分布式的重入锁
、读写锁
等等,读者完全可以放心大胆的使用Redisson实现分布式锁。
Redisson的时候场景非常多,由于篇幅现在这里笔者就点到为止。建议去Redisson官网去进一步学习。
一个合格的Redis分布式锁考虑到多少问题。在实际使用中还需要考虑到KEY的设计以及锁的粒度问题。粒度越低影响越少,只锁定访问共享数据的代码尽可能的降低锁的粒度。
Redis分布式锁就介绍到这里,看到这里的你是否有所收获呢?