在使用分布式锁时,如果单节点的Redis发生故障,则整个业务的分布式锁都将无法使用。
如果是主从模式或者集群模式下的Redis发生故障,由于Master 节点是独立的数据不同步,在主从同步的期间,Master 节点发生故障,即使Slaver 节点被选举为 Master 节点,分布式锁的Key信息可能就会丢失,锁失效的情况。
对于Redis
集群模式下,为了解决Master 节点发生故障
带来的问题,通过Redisson
封装的RedLock
可以进行解决该问题;
机制流程如下:
1、依次向 N 个 Redis 服务发出请求,用能够保证全局唯一的 value 申请锁 key;
2、如果从 N/2+1 个 redis 服务中都获取锁成功,那么,本次分布式锁的获取被视为成功,否则视为获取锁失败。
3、如果获取锁失败,或执行达到 超时时间,则向所有 Redis 服务都发出解锁请求。
分布式事务锁最常见的一个问题就是如果已经获取到锁的 client 在 TTL 时间内没有完成竞争资源的处理,而此时锁会被自动释放,造成竞争条件的发生。
这种情况如果让 client 端设置定时任务自动延长锁的占用时间,会造成 client 端逻辑的复杂和冗余。
redisson 在实现的过程中,自然也考虑到了这一问题,redisson 提供了一个“看门狗”的可选特性,并且增加了 lockWatchdogTimeout 配置参数,看门狗线程会自动在 lockWatchdogTimeout 超时后顺延锁的占用时间,从而避免上述问题的发生。
但是,由于看门狗作为独立线程存在,对于性能有所影响,如果并非是处理高度竞争且处理时长不固定的特殊资源,那么并不建议启用 redisson 的看门狗特性。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>3.13.6version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.8version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
dependencies>
spring:
#redis配置
redis:
# Redis数据库索引(默认为0)
database: 1
# 连接地址
host: 162.14.115.18
#端口号
port: 6379
##连接超时时间
timeout: 3600
#密码
password: 123456
# 集群模式
# cluster:
# nodes: 162.14.115.18:6379,162.14.115.18:6379
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 1
#关闭超时
shutdown-timeout: 500ms
# 哨兵模式
# sentinel:
# master: master
# nodes: 162.14.115.18:6379,162.14.115.18:6379
@Component
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfigProperties {
private String host;
private String port;
private String password;
private Cluster cluster;
private Integer timeout;
private Sentinel sentinel;
@Data
public static class Sentinel {
private String master;
private List<String> nodes;
public List<String> getNodes() {
return nodes;
}
public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
}
@Data
public static class Cluster {
private List<String> nodes;
public List<String> getNodes() {
return nodes;
}
public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
}
}
@Configuration
public class RedissonConfig {
@Resource
private RedissonConfigProperties redissonConfigProperties;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
String redisPort = redissonConfigProperties.getPort();
Integer redisTimeout = redissonConfigProperties.getTimeout();
String redisPassword = redissonConfigProperties.getPassword();
RedissonConfigProperties.Cluster cluster = redissonConfigProperties.getCluster();
RedissonConfigProperties.Sentinel sentinel = redissonConfigProperties.getSentinel();
// 集群
if (cluster != null && !CollectionUtils.isEmpty(cluster.getNodes())) {
List<String> nodes = cluster.getNodes();
List<String> collect = nodes.stream().map(x -> x = x.startsWith("redis://") ? x : "redis://" + x).collect(Collectors.toList());
ClusterServersConfig serverConfig = config.useClusterServers()
.setScanInterval(2000)
.addNodeAddress(collect.toArray(new String[collect.size()]));
if (StringUtils.isNotBlank(redisPassword)) {
serverConfig.setPassword(redisPassword);
}
}
//哨兵
else if (sentinel != null && !CollectionUtils.isEmpty(sentinel.getNodes())) {
List<String> nodes = sentinel.getNodes();
List<String> collect = nodes.stream().map(x -> x = x.startsWith("redis://") ? x : "redis://" + x).collect(Collectors.toList());
SentinelServersConfig serverConfig = config.useSentinelServers()
.setMasterName(sentinel.getMaster())
.addSentinelAddress(collect.toArray(new String[collect.size()]))
.setTimeout(redisTimeout);
if (StringUtils.isNotBlank(redisPassword)) {
serverConfig.setPassword(redisPassword);
}
} else {
// 单机配置
String redisHost = redissonConfigProperties.getHost();
redisHost = redisHost.startsWith("redis://") ? redisHost : "redis://" + redisHost;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(redisHost + ":" + redisPort)
.setTimeout(redisTimeout);
if (StringUtils.isNotBlank(redisPassword)) {
serverConfig.setPassword(redisPassword);
}
}
return Redisson.create(config);
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
/**
* 上锁的方法中,key所在参数的位置,索引从0开始
*
* @return
*/
int keyNum();
/**
* 上锁时长,默认设置时间
*
* @return
*/
long lockTime() default -1;
/**
* 尝试时间,设置时间内通过自旋一致尝试获取锁,默认0ms 通常时间要小于lockTime时间
*
* @return
*/
long tryTime() default 0;
}
@Aspect
@Component
@Slf4j
public class RedisLockAspect {
@Resource
private RedissonClient redissonClient;
private static final ThreadLocal<RLock> LOCK_THREAD = new ThreadLocal<>();
@Pointcut("@annotation(lhz.lx.aspect.RedisLock)")
public void lockPoint() {
}
/**
* 环绕通知,调用目标方法
*/
@Around("lockPoint()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 记录方法执行开始时间
long startTime = System.currentTimeMillis();
Object[] args = proceedingJoinPoint.getArgs();
if (args.length <= 0) {
throw new RuntimeException("keyName不存在!");
}
String[] argNames = ((CodeSignature) proceedingJoinPoint.getSignature()).getParameterNames();
Signature signature = proceedingJoinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
RedisLock lock = method.getAnnotation(RedisLock.class);
int keyNum = lock.keyNum();
if (!"keyName".equals(argNames[keyNum])) {
throw new RuntimeException("keyName不存在!");
}
String key = args[keyNum].toString();
long lockTime = lock.lockTime();
long tryTime = lock.tryTime();
log.info("分布式锁上锁,key:{},lockTime:{}", key, lockTime);
RLock clientLock = redissonClient.getLock(key);
// 尝试加锁,最多等待 tryTime 毫秒,上锁以后 lockTime 毫秒自动解锁
// 如果lockTime不设置,则会启动看门狗机制(默认30S),每10S会自动续锁
boolean locked = clientLock.tryLock(tryTime, lockTime, TimeUnit.MILLISECONDS);
if (!locked) {
log.error("上锁失败");
// 如果为了不影响业务,可以不抛出异常,继续向下执行
throw new RuntimeException("上锁失败!");
}
clientLock.lock(lockTime, TimeUnit.MILLISECONDS);
log.info("分布式锁上锁成功,key:{},lockTime:{}", key, lockTime);
LOCK_THREAD.set(clientLock);
// 调用目标方法
return proceedingJoinPoint.proceed();
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(value = "lockPoint()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
handleData();
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "lockPoint()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleData();
}
private void handleData() {
RLock clientLock = LOCK_THREAD.get();
if (clientLock != null) {
try {
log.info("任务执行完成,当前锁状态:{}", clientLock.isLocked());
// 无需判断锁是否存在,直接调用unlock
clientLock.unlock();
} catch (Exception exception) {
exception.printStackTrace();
} finally {
LOCK_THREAD.remove();
}
}
}
}
因为说明AOP无法拦截类内部的方法之间的调用,需要对启动类加上@EnableAspectJAutoProxy
配置,代码如下:
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class RedisDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RedisDemoApplication.class, args);
}
}
注意: 在调用使用分布式锁的方法时
TestController:
@RestController
@RequestMapping
@Slf4j
public class TestController {
@Resource
private RedisService redisService;
@GetMapping(value = "/testLock")
public String testLock() {
String key = "testKey";
// String key = UUID.randomUUID().toString();
redisService.testLock(key);
return "success";
}
}
RedisService :
public interface RedisService {
/**
* 方法直接调用使用锁
*
* @param keyName
*/
void testLock(String keyName);
}
RedisServiceImpl:
@Service
@Slf4j
public class RedisServiceImpl implements RedisService{
/**
* 方法直接调用使用锁
*/
@Override
@RedisLock(keyNum = 0, lockTime = 1000, tryTime = 500)
public void testLock(String keyName) {
log.info("方法直接调用使用锁");
try {
// 如果业务执行过长
// Thread.sleep(3200);
System.out.println("加锁成功,执行业务逻辑");
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试一:
正常上锁情况
测试二:
设置tryTime,并且tryTime小于lockTime
;当这样配置时,在第一个线程没有结束时,第二个线程,超过tryTime
就会出现上锁失败;
修改代码如下:
@RedisLock(keyNum = 0,lockTime = 3000,tryTime = 2000)
public void testLock(String keyName) throws InterruptedException {
log.info(keyName);
TimeUnit.SECONDS.sleep(5);
}
快速请求两次
接口,截图如下:
通过截图可以看到,在第一个线程上锁后,过了2000ms
出现了上锁失败的提示;
测试三:
设置tryTime,并且tryTime大于lockTime;当这样配置时,不会出现上锁失败,并且第二个线程会一直等到第一个线程结束;
修改代码如下:
@RedisLock(keyNum = 0,lockTime = 3000,tryTime = 4000)
public void testLock(String keyName) throws InterruptedException {
log.info(keyName);
TimeUnit.SECONDS.sleep(5);
}
快速请求两次
接口,截图如下:
通过截图可以看到,在第一个线程上锁后,超过了3000ms
后,第二个线程开始上锁成功