浅谈分布式锁
一、 现象实例
1.修改定时任务
`package com.qfedu.springboot03.task;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component
public class PrintTask {
@Autowired
private RedissonClient redissonClient;
// 任务默认同步执行的
@Async // 表示异步执行的注解
// 每分钟的第5秒触发定时任务
@Scheduled(cron = "5 * * * * ?")
public void printInfo() {
System.out.println(count);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS");
System.out.println(Thread.currentThread().getName() + ":" + sdf.format(new Date()));
}
}
2.启动同一个项目的多个实例
配置:
切换不同的配置,启动项目
3、观察执行结果
两个项目实例的任务"同时"执行了
如果不想任务同时执行,需要怎么办?这时,可以借助分布式锁实现。
二、分布式锁介绍
随着业务的发展,传统单机部署无发很好的解决高并发的问题,于是,原来的单机部署演化成分布式集群部署,如果在集群环境下,我们想针对某个共享的操作加锁,这时用传统的策略就无法实现。为了解决这个问题,就需要一种跨机器的互斥机制来控制共享资源的访问,通过分布式锁就能解决这样的问题。
注意:本文通过Redission框架(相当于redis的客户端工具),借助redis实现分布式锁。
三、分布式锁的解决方案
redis命令
(1)setnx key value + expire(超时设置)
(2)SET key value ex seconds nx,相当于setnx 和 expire的组合,2.8版本后提供的命令ex seconds 过期时间,防止加锁线程死掉不能解锁nx 如果没有这个key则设置,存在key返回失败。
比如:set name zhang ex 300 nx
RedLock
http://redis.cn/topics/distlock.html
在redis集群环境下,上面的方案并不是绝对安全的。
下面提到的Redission提供了红锁的机制。
第三方工具Redission
注意,图片来自网络
线程去获取锁,获取成功则执行lua脚本,保存数据到redis数据库。
如果获取失败,一直通过while循环尝试获取锁(相关方法中可设置获取锁的等待时间,如果超时返回失败),获取成功后,执行lua脚本,保存数据到redis数据库。
Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制。
Redission实现分布式锁
导入jar
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.18.0</version>
</dependency>
<!-- 如果项目中,导入过redis的包,这里直接导入redisson的包即可 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.18.0</version>
</dependency>
配置类
以单机redis为例
package com.qfedu.springboot03.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient getRedisson() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setRetryInterval(5000)
.setTimeout(10000)
.setConnectTimeout(10000);
return Redisson.create(config);
}
}
修改SpringTask任务
核心方法:tryLock()
`package com.qfedu.springboot03.task;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component
public class PrintTask {
@Autowired
private RedissonClient redissonClient;
// 任务默认同步执行的
@Async // 表示异步执行的注解
@Scheduled(cron = "5 * * * * ?")
public void printInfo() {
// 获取锁对象,参数表示redis中锁的key值
RLock lock = redissonClient.getLock("testlock");
try {
// 尝试加锁,如果可以加锁,返回true
// 第一个参数表示加锁等待的延迟
// 第二个参数表示锁的自动释放时间
if (lock.tryLock(0, 10, TimeUnit.SECONDS)) {
System.out.println(count);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS");
System.out.println(Thread.currentThread().getName() + ":" + sdf.format(new Date()));
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (taskLock.isLocked()) {
// 判断是否当前线程拥有该锁
if (taskLock.isHeldByCurrentThread()) {
// 释放锁
taskLock.unlock();
}
}
}
}
}
`