SpringBoot整合Redis实现分布式锁
在单体项目中,我们通过锁解决的是控制共享资源访问 的问题,而分布式锁,就是解决分布式系统中控制共享资源访问 的问题,与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程 。一个分布式锁应该具备下列条件:
1. 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
2. 高可用的获取与释放锁
3. 高性能的获取与释放锁
4. 具备可重入特性(可理解为重新进入,由多于一根任务并发使用,而不必担心数据错误)
5. 具备锁失效机制,即自动解锁,防止死锁
6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
分布式锁实现方式:
1. 基于数据库实现分布式锁
2. 基于zookeeper实现分布式锁
3. 基于redis实现分布式锁
基于redis 的分布式锁
常用命令:
1. 基于setnx命令的分布式锁
setnx: set if not exist ,当且仅当key不存在时,将key的值设置为val,key存在时,不做任何动作
命令格式: setnx lock value
(1)存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,那么竞争该锁的线程都会执行不了,产生死锁的情况。
(2)解决方案:设置锁超时时间
2. 基于setnx命令加锁并设置锁过期时间
命令格式: set key value nx ex expireTime
(1)存在问题:
① 假如线程A成功得到了锁,并且设置的超时时间是 30 秒。如果某些原因导致线程 A 执行的很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。
② 随后,线程A执行完任务,接着执行del指令来释放锁。但这时候线程 B 还没执行完,线程A实际上删除的是线程B加的锁。
(2)解决方案:
可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。但是,这样做其实隐含了一个新的问题,get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性
3. 锁续期
让获得锁的线程开启一个守护线程,用来给快要过期的锁“续期
① 假设线程A执行了29 秒后还没执行完,这时候守护线程会执行 expire 指令,为这把锁续期 20 秒。守护线程从第 29 秒开始执行,每 20 秒执行一次。
② 情况一:当线程A执行完任务,会显式关掉守护线程。
③ 情况二:如果服务器忽然断电,由于线程 A 和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
4. 演示代码
演示代码如下:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.0</version>
</parent>
<groupId>com.young</groupId>
<artifactId>redisson01</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.18.1</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
application.yml
spring:
redis:
host: localhost
port: 3306
jedis:
pool:
min-idle: 0
max-idle: 8
max-wait: 1000
max-active: 8
timeout: 5000
server:
port: 8090
Application.java
package com.young;
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
@Bean
public Redisson redisson(){
//此时为单机模式
Config config=new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
IndexController.java
package com.young;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
public class IndexController {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//高并发下会出现超买,没有加锁
@GetMapping("/reduct01")
public String reduct01(){
Integer stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock<0){
System.out.println("扣减失败,库存不足");
}else{
Integer realStock=stock-1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:"+realStock);
}
return "end";
}
//高并发下单机模式不会出现超买,但是在分布式环境下,仍然会出现超买的现象
@GetMapping("/reduct02")
public String reduct02(){
synchronized (this) {
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock < 0) {
System.out.println("扣减失败,库存不足");
} else {
Integer realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
}
}
return "end";
}
//通过setnx key value命令,对商品库存进行加锁
@GetMapping("/reduct03")
public String reduct03(){
//这里的key一般设置为和商品有关的key,以提高性能
String lockKey="product_01";
//这里可以将value设置为线程id
String value=UUID.randomUUID().toString();
// stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"setnx"); -> setnx key value
// stringRedisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
//设置setnx和超时时间要原子操作
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, 10, TimeUnit.SECONDS);
if (!flag){
//加锁失败
return "error code";
}
/**
* 因为在操作的过程中,可能会抛出异常,因此我们要用try将代码块包起来,然后在finally中,释放锁,否则会出现因为异常退出,但锁在redis中存在,从而
* 使其他线程在访问该商品时加锁失败,无法顺利扣减库存
*/
try {
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock < 0) {
System.out.println("扣减失败,库存不足");
} else {
Integer realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
}
}finally {
//双重保证,将value设置为当前线程的id,保证锁不会被其他线程删掉
if (value.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
//删除锁
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
/**
* 在上面的reduct03代码中,我们通过setnx key value进行加锁,我们设置的超时时间是10秒,假设当前商品库存为100,现在线程A访问该方法,获取到
* 锁,然后在执行业务代码时,执行的时间超过了10秒,此时锁过期了,这时线程B访问该方法,获取到锁,获取商品库存为100,这样当A和B线程都执行完毕后,
* 商品的库存实际上,只减少了1,也就是变成99,从而导致超买,那么我们需要在锁过期前,进行续期,因此使用redisson
* @return
*/
@GetMapping("/reduct04")
public String reduct04(){
String lockKey="product_01";
RLock redissonLock = redisson.getLock(lockKey);
try {
redissonLock.lock();
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock < 0) {
System.out.println("扣减失败,库存不足");
} else {
Integer realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
}
}finally {
redissonLock.unlock();
//删除锁
stringRedisTemplate.delete(lockKey);
}
return "end";
}
/**
* 考虑一种情况,我们使用redis集群,当设置key的时候,主节点甲会异步地将数据同步到其他从节点,假设现在线程A扣减商品product1库存,那么它在主节点A中设置可以,
* 但是假设主节点A在同步数据之前挂了,数据没同步到从节点,现在从节点乙推选为主节点,线程B访问主节点乙,然后也是访问商品product1,设置key值,然后假设A、B线
* 程同时结束,这个时候,会出现超买问题。根本原因是Redis主从同步是异步的,我们可以使用zookeeper来加分布式锁,zookeeper在主从同步时,主节点设置成功后,会
* 先同步给从节点,只有集群中有一半以上同步后,才返回true,而即使主节点挂了,在推选主节点的时候,选择的也会是数据最完善的那个,因此不会出现刚才的问题,但带来
* 的问题就是,zookeeper的性能比redis差
*/
// @RequestMapping("/deduct_stock")
// public String deductStock(){
//锁住的lock要以资源为名,提高效率
// String lockKey="product_001";
//
// //可以设置为线程id
// String clientId= UUID.randomUUID().toString();
//
// //jedis.setnx(key,value)
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);
//
// //加锁和设置过时时间要原子操作,考虑问题,执行时间大于超时时间?
// Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
//
// if (!result){
// return "error_code";
// }
//
// try{
// int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock");
// if (stock>0){
// int realStock=stock-1;
// stringRedisTemplate.opsForValue().set("stock",realStock+"");
// System.out.println("扣减成功,剩余库存:"+realStock);
// }else{
// System.out.println("扣减失败,库存不足");
// }
// }finally {
// if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
// //当前线程的id和取出来的id相同时,才解锁
// stringRedisTemplate.delete(lockKey);
// }
// }
// return "end";
//
// String lockKey="product_001";
//
// RLock redissonLock = redisson.getLock(lockKey);
//
// try{
// redissonLock.lock();
// int stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// if (stock>10){
// int realStock=stock-1;
// stringRedisTemplate.opsForValue().set("stock",realStock+"");
// System.out.println("扣减成功,剩余库存:"+realStock);
// }else{
// System.out.println("扣减失败,库存不足");
// }
// }finally {
// redissonLock.unlock();
// }
// return "end";
// }
}