1.使用场景
由于当今软件的部署方式大多数都是以分布式的方式进行部署的,那么由于这种部署形式的盛行,基于JVM的锁在并发量大的情况下仍有可能出现BUG。例如在秒杀,抢购等场景中,当所有线程都使用同一资源的情况下,JVM的锁是不再适用的。所以推行分布式锁进行资源管理。
2.架构图
由图可见,当每个请求发送到单个JVM的时候,是无法保证资源的同一性,在秒杀等一些场景当中,仍然会出现超卖现象。所以需要使用一个公有资源来完成锁上锁的行为。而基于Redis的分布式锁便可以很好的实现。
首先Redis本身就是单线程的,所以可以保证在任何一个时间节点下,只有一个线程可以访问;其次Redis是基于内存的存储单元,效率很高。所以使用Redis来做分布式锁是十分合适的。
3.分布式锁的实现
@RestController
public class RedisLockController {
@Autowired
RedisTemplate redisTemplate;
@RequestMapping("/doSkillMethod")
public String doLockServer(){
//设置一个锁的key
String lockKey = "lock";
//使用redis的setnx方法来确定程序是否已经加锁
boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,"success");
//如果已上锁,则直接返回
if(!isLock){
return "false";
}
//执行对应的业务
Integer stock = (Integer) redisTemplate.opsForValue().get("stock");
if(stock>0){
stock --;
redisTemplate.opsForValue().set("stock", stock);
}else{
return "库存不足";
}
//释放锁
redisTemplate.delete("stock");
return "true";
}
}
该程序可以简单的实现分布式锁。逻辑主要是利用redis的setNX方法(如果key不存在才会设置,存在则不会设置),相当于在内存中加一个标记,当每个线程调用该方法后,首先都会先从redis中查看该标记是否已经被标记。
但是该分布式锁却存在很多BUG,不可以投入到生产环境中。
问题:
1.当程序加锁后,运行当中出现了异常,则会无法释放锁,导致死锁。
2.在程序运行途中,服务器宕机,导致程序没有跑完,锁没有释放而导致死锁。
解决方法:
1.在程序段上增加try,finally。保证一定会释放锁。
2.在设置锁的时候增加过期时间,到时间自动删除锁。
代码改进:
@RestController
public class RedisLockController {
@Autowired
RedisTemplate redisTemplate;
@RequestMapping("/doSkillMethod")
public String doLockServer(){
//设置一个锁的key
String lockKey = "lock";
try{
//使用redis的setnx方法来确定程序是否已经加锁,设置过期时间为10s
boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,"success", 10, TimeUnit.SECONDS);
//如果已上锁,则直接返回
if(!isLock){
return "false";
}
//执行对应的业务
Integer stock = (Integer) redisTemplate.opsForValue().get("stock");
if(stock>0){
stock --;
redisTemplate.opsForValue().set("stock", stock);
}else{
return "库存不足";
}
}finally {
//释放锁,若出现异常,则也会必定释放锁
redisTemplate.delete("stock");
return "true";
}
}
}
给锁设置了过期时间后又会出现新的BUG。例如当线程A先获得了锁,但是在10s的过期时间内由于各种原因并没有运行完程序,A上的锁则会自动消失;而此时线程B则会进入,加锁。而此时,线程A正好完成了所有的程序步骤,则会将线程B的锁释放掉。于是就会出现A释放B的锁,B释放C的锁。。。。导致程序紊乱,超卖显现再次发生。所以该方式的分布式锁也是不可以投入生产环境的。
于是我们给锁设置一个唯一的值,用于标记。保证每个线程只会删除自己的锁,不会误删其他线程的锁。
@RestController
public class RedisLockController {
@Autowired
RedisTemplate redisTemplate;
@RequestMapping("/doSkillMethod")
public String doLockServer(){
//设置一个锁的key
String lockKey = "lock";
//设置唯一锁的值
String uLockValue = UUID.randomUUID().toString();
try{
//使用redis的setnx方法来确定程序是否已经加锁,设置过期时间为10s
boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uLockValue, 10, TimeUnit.SECONDS);
//如果已上锁,则直接返回
if(!isLock){
return "false";
}
//执行对应的业务
Integer stock = (Integer) redisTemplate.opsForValue().get("stock");
if(stock>0){
stock --;
redisTemplate.opsForValue().set("stock", stock);
}else{
return "库存不足";
}
}finally {
//判断当前锁是否是自己设置的锁
if(uLockValue.equals(redisTemplate.opsForValue().get(lockKey))){
//释放锁,若出现异常,则也会必定释放锁
redisTemplate.delete("stock");
return "true";
}else{
return "true";
}
}
}
}
但是,该方法只能解决每个线程只能释放自己的锁,而没办法解决锁自动过期后其他线程进入程序的BUG。
面对这一问题,我们可以在设置锁后抛出一个新的线程,每过一段时间(小于锁的设置时间),查看当前锁是否还存在,如果存在则将锁的时间设置为原先的过期时间(给锁续时)。
@RestController
public class RedisLockController {
@Autowired
RedisTemplate redisTemplate;
@RequestMapping("/doSkillMethod")
public String doLockServer(){
//设置一个锁的key
String lockKey = "lock";
//设置唯一锁的值
String uLockValue = UUID.randomUUID().toString();
//声明一个线程
Thread thread = null;
try{
//使用redis的setnx方法来确定程序是否已经加锁,设置过期时间为10s
boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uLockValue, 10, TimeUnit.SECONDS);
//如果已上锁,则直接返回
if(!isLock){
Thread.sleep(100);
doLockServer();
}
//给锁续时(抛出一个新的线程,在新的线程里面重新设置过期时间,时间间隔为过期时间的三分之一比较合适)
thread = setTimer(lockKey, uLockValue);
//执行对应的业务
Integer stack = (Integer) redisTemplate.opsForValue().get("stackNum");
System.out.println(stack);
if(stack>0){
stack --;
System.out.println(stack);
redisTemplate.opsForValue().set("stackNum", stack);
}else{
return "库存不足";
}
}finally {
//结束计时器线程
thread.stop();
//判断当前锁是否是自己设置的锁
if(uLockValue.equals(redisTemplate.opsForValue().get(lockKey))){
//释放锁,若出现异常,则也会必定释放锁
redisTemplate.delete("lockKey");
return "true";
}else{
return "true";
}
}
}
public Thread setTimer(String lockKey, String uLockValue){
final long timeInterval = 3000;
Runnable runnable = new Runnable(){
public void run() {
while(true){
try {
Thread.sleep(timeInterval);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(uLockValue.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
}
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();
return thread;
}
}
添加一个计时器,可以有效的保证锁不会提前过期。一般在生产环境当中,大多数都会使用Redisson框架来实现分布式锁。
4.使用Redisson实现分布式锁
Redisson是一个基于Java实现的一个Redis客户端框架,主要运用于分布式架构。使用内部的锁架构可以有效的避免超卖等问题的发生。
1.添加Redisson的依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
2.添加配置文件
@Configuration
public class RedissonConfig {
@Bean
public Redisson redisson(){
Config config = new Config();
//设置为单机地址的配置
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
3.实现分布式锁
@RestController
public class RedissonLockController {
@Autowired
private Redisson redisson;
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/doSkillMethodRedisson")
public String doRedissonLockServer(){
//生成一个锁
RLock redissonLock = redisson.getLock("lockKey");
try{
//给锁设置过期时间(没有参数会打开看门狗模式,从而启动一个守护线程,动态更新锁的时间)
redissonLock.lock();
//获取库存数量
int stackNum = (int) redisTemplate.opsForValue().get("stackNum");
if(stackNum>0){
stackNum --;
redisTemplate.opsForValue().set("stackNum", stackNum);
}else{
System.out.println("库存不足!");
}
}finally {
//释放锁
redissonLock.unlock();
}
return "true";
}
}