分布式锁(多服务共享锁) 在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
默认的加锁逻辑是非公平的。在加锁失败时,线程会进入 while 循环,一直尝试获得锁,这时候是多线程进行竞争。就是说谁抢到就是谁的。Redisson 提供了公平锁机制,使用方式如下
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
看门狗机制是在 RedissonBaseLock#scheduleExpirationRenewal 方法中,这块公平锁和非公平锁并无区别。前文已经了解到,公平锁加锁失败之后,会将当前放到等待队列中,通过 Java 代码中的循环不断尝试获得锁。
公平锁的释放同样分为主动释放和超时释放。
需求
当在打车软件中,乘客下了订单。多个司机抢单,此时因为单子只有一个,多个司机对此共享资源进行抢,此处应该使用分布式锁;后台服务部署在多台服务器上;
controller层代码
@GetMapping("/do/{orderId}")
public String grab(@PathVariable("orderId") int orderId, int driverId){
System.out.println("order:"+orderId+",driverId:"+driverId);
//此处调用锁控制层代码
grabService.grabOrder(orderId,driverId);
return "";
}
锁控制层代码(使用synchronized 不成功)
使用synchronized 不能保证多台服务器只有一个抢成功;因为synchronized 只能锁本服务的资源;多台服务的资源是锁不住的;
@Autowired
OrderService orderService;
@Override
public String grabOrder(int orderId, int driverId) {
String lock = (orderId+"");
synchronized (lock.intern()) {
try {
System.out.println("司机:"+driverId+" 执行抢单逻辑");
//此处调用订单业务代码
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
} finally {
}
}
return null;
}
红锁本质上就是使用多个Redis做锁。例如有5个Redis,一次锁的获取,会对每个请求都获取一遍,如果获取锁成功的数量超过一半(2.5),则获取锁成功,反之失败;
释放锁也需要对每个Redis释放
单实例肯定不是很可靠吧?加锁成功之后,结果 Redis 服务宕机了,这不就凉凉~
这时候会提出来将 Redis 主从部署。即使是主从,也是存在巧合的!
主从结构中存在明显的竞态:
客户端 A 从 master 获取到锁在 master 将锁同步到 slave 之前,master 宕掉了。
slave 节点被晋级为 master 节点客户端 B 取得了同一个资源被客户端 A 已经获取到的另外一个锁。安全失效!有时候程序就是这么巧,比如说正好一个节点挂掉的时候,多个客户端同时取到了锁。如果你可以接受这种小概率错误,那用这个基于复制的方案就完全没有问题。
那我使用集群呢?如果还记得前面的内容,应该是知道对集群进行加锁的时候,其实是通过 CRC16 的 hash 函数来对 key 进行取模,将结果路由到预先分配过 slot 的相应节点上。
发现其实还是发到单个节点上的! 这时候 Redis 作者提出了 RedLock 的概念
总结一下就是对集群的每个节点进行加锁,如果大多数(N/2+1)加锁成功了,则认为获取锁成功。
RedLock 的问题
看着 RedLock 好像是解决问题了:
客户端 A 锁住了集群的大多数(一半以上);
客户端 B 也要锁住大多数;
这里肯定会冲突,所以 客户端 B 加锁失败。
那实际解决问题了么?
有一个很大的疑问,我加锁 lock1、lock2、lock3,但是 RedissonRedLock 是如何保证这三个 key 是在归属于 Redis 集群中不同的 master 呢?因为按照 RedLock 的理论,是需要在半数以上的 master 节点加锁成功。阅读完源码之后,发现 RedissonRedLock 完全是 RedissonMultiLock 的子类,只是重写了 failedLocksLimit 方法,保证半数以上加锁成功即可。所以这三个 key,是需要用户来保证分散在不同的节点上的。
那我使用 5 个单节点的客户端,然后再使用红锁,听着好像是可以的,并且 RedissonRedLock 可以这样使用。但是那和 Redis 集群还有啥关系啊!所以依然没有解决我的问题,在 redis 集群下 针对master节点集群,,还是需要用户自己来“手工定位锁”,使锁的节点分散到不同的master 集群节点下。 手工定位锁,这个…… 我考虑了下,还是不用 RedLock 吧!如果master节点变动则锁也存在问题,master集群同步等等锁的同步和锁失效也是需要考虑的问题;
Redisson 的开发者认为 Redis 的红锁也存在争议(前文介绍的那个争议),但是为了保证可用性,RLock 对象执行的每个 Redis 命令执行都通过 Redis 3.0 中引入的 WAIT 命令进行同步。
Redisson 的开发者认为 Redis 的红锁也存在争议(前文介绍的那个争议),但是为了保证可用性,RLock 对象执行的每个
Redis 命令执行都通过 Redis 3.0 中引入的 WAIT 命令进行同步。
源码在这一部分。
看源码,同时发送了一个 WAIT 1 1000 到 Redis。
结论:Redisson RedLock 是基于联锁 MultiLock 实现的,但是使用过程中需要自己判断 key 落在哪个节点上,对使用者不是很友好。
基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
大家都知道,如果负责储存某些分布式锁的某些Redis节点宕机以后,而且这些锁正好处于锁住的状态时,这些锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
@Component
public class RedisConfig {
@Bean(name = "redissonRed1")
@Primary
public RedissonClient redissonRed1(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6379").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonRed2")
public RedissonClient redissonRed2(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6380").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonRed3")
public RedissonClient redissonRed3(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6381").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonRed4")
public RedissonClient redissonRed4(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6382").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonRed5")
public RedissonClient redissonRed5(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6383").setDatabase(0);
return Redisson.create(config);
}
}
配置方式2
基于 Redis 的 Redisson 分布式联锁 RedissonMultiLock 对象可以将多个 RLock 对象关联为一个联锁,每个 RLock 对象实例可以来自于不同的 Redisson 实例。
按照官方文档的说法,这里 Redisson 客户端可以不是同一个。当然,一般工作中也不会说不用一个客户端吧,可以看出 遍历所有的锁,依次加锁。加锁逻辑就和可重入锁加锁并无区别了。所以 Lua 脚本就不进行分析了
package com.online.taxi.order.service.impl;
import com.online.taxi.order.constant.RedisKeyConstant;
import com.online.taxi.order.service.GrabService;
import com.online.taxi.order.service.OrderService;
import org.redisson.Redisson;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class GrabRedisRedissonRedLockLockServiceImpl implements GrabService {
// 红锁
@Autowired
@Qualifier("redissonRed1")
private RedissonClient redissonRed1;
@Autowired
@Qualifier("redissonRed2")
private RedissonClient redissonRed2;
@Autowired
@Qualifier("redissonRed3")
private RedissonClient redissonRed3;
@Autowired
@Qualifier("redissonRed4")
private RedissonClient redissonRed4;
@Autowired
@Qualifier("redissonRed5")
private RedissonClient redissonRed5;
@Autowired
OrderService orderService;
@Override
public String grabOrder(int orderId , int driverId){
System.out.println("红锁实现类");
//生成key
String lockKey = ("" + orderId).intern();
//redisson锁 单节点
// RLock rLock = redissonRed1.getLock(lockKey);
//红锁 redis son
RLock rLock1 = redissonRed1.getLock(lockKey);
RLock rLock2 = redissonRed2.getLock(lockKey);
RLock rLock3 = redissonRed3.getLock(lockKey);
RLock rLock4 = redissonRed4.getLock(lockKey);
RLock rLock5 = redissonRed5.getLock(lockKey);
RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3,rLock4,rLock5);
try {
/**红锁
* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
* leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
*/
boolean b1 = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
if (b1){
System.out.println("加锁成功");
// 此代码默认 设置key 超时时间30秒,过10秒,再延时
System.out.println("司机:"+driverId+" 执行抢单逻辑");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
System.out.println("加锁成功");
}else {
System.out.println("加锁失败");
}
} finally {
rLock.unlock();
}
return null;
}
}
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.18.0</version>
</dependency>
@Value("${spring.redis.host}")
private String url;
@Value("${spring.redis.port}")
private Integer port;
@Value("${spring.redis.password}")
private String password;
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
log.info("Redisson 初始化 {}",String.format("redis://%s:%d",url,port));
config.useSingleServer().setAddress(String.format("redis://%s:%d",url,port) )
.setPassword(password);
// 创建RedissonClient对象
return Redisson.create(config);
}
此处枷锁后在释放锁是解决了锁的读写机制问题
@Autowired
private RedissonClient redissonClient;
public List<? extends Object> getCachedConfigList(String ode) {
List<StructureVo> configuration =null;
String key = getCacheKey(orgCode, STR_CONFIG_LIST);
List<Object> cacheConfig = redisCache.getCacheList(key);
if (!CollectionUtils.isEmpty(cacheConfig)) {
return cacheConfig;
}
RLock lock = redissonClient.getLock(getReddisonEbomCacheKey());
try {
lock.lock(DEFAULT_EXPIRE_SECOND,TimeUnit.SECONDS);
List<Object> secondCacheConfig = redisCache.getCacheList(key);
if (!CollectionUtils.isEmpty(secondCacheConfig)) {
log.info("second lock reddison 查询到缓存释放锁!");
reddisnUnlock(getReddisonEbomCacheKey());
return cacheConfig;
}
log.info("reddison枷锁成功!存放缓存资源!");
configuration = this.queryAllConfiguration();
redisCache.setCacheList(key, configuration);
redisCache.expire(key, DEFAULT_EXPIRE_HOURS, TimeUnit.HOURS);
} catch (Exception e) {
log.error("Happen Exception: " + e.getMessage(),e);
}finally {
reddisnUnlock(getReddisonEbomCacheKey());
}
return configuration;
}
释放锁方法
private void reddisnUnlock(String key) {
log.info(" reddison key {}锁释放!",key);
try {
RLock unlock = redissonClient.getLock(key);
if (unlock != null && unlock.isHeldByCurrentThread()) {
unlock.unlock();
}
} catch (IllegalMonitorStateException e) {
log.info("reddison锁释放 Exception: " + e.getMessage(),e);
}
}
单实例加锁
SET resource_name my_random_value NX PX 30000
对于单实例 Redis 只需要使用这个命令即可。
简单描述即为
可以通过以下 Lua 脚本实现锁释放:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
为什么要设置随机值?
主要是为了防止锁被其他客户端删除。有这么一种情况:
。
1.减少开销–减少向redis服务器的请求次数
2.原子操作–redis将lua脚本作为一个原子执行
3.可复用–其他客户端可以使用已经执行过的lua脚本
4.增加redis灵活性–lua脚本可以帮助redis做更多的事情