如上图,请求A、B、C都是发起扣减同一个商品的库存操作,三个请求被分发到三台不同的服务部署机器上进行处理。而三台机器并不在同一个JVM,所以Java提供的线程同步技巧就发挥不了作用了。但是对于扣减库存这样的场景,必须要使用线程同步来保证同一个商品的库存不会被漏扣或者多扣。
为了保证在高并发的场景下,临界资源(共享资源)同时只能被一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。在单机环境中,Java中提供了很多并发处理相关的API。
但是在分布式系统中,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
首先来引入下依赖,其实不需要这么多,我太懒了,就不改了
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.4.RELEASEversion>
parent>
<dependencies>
<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.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.9.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.10version>
dependency>
<dependency>
<groupId>javax.persistencegroupId>
<artifactId>persistence-apiartifactId>
<version>1.0version>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>28.1-jreversion>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>net.minidevgroupId>
<artifactId>json-smartartifactId>
<version>2.3version>
<scope>compilescope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
编写启动类
package com.cmdc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RedissonApplication {
/**
* 启动类方法
* @param args 参数
*/
public static void main(String[] args) {
SpringApplication.run(RedissonApplication.class,args);
}
}
编写springboot默认会读取的配置文件application.yml(其实你不写也会走默认)
server:
port: 8111
spring:
application:
name: springboot-redisson
编写redisson需要的配置文件redisson-config.yml
#Redisson配置
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: null
clientName: null
database: 7 #选择使用哪个数据库0~15
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
subscriptionsPerConnection: 5
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
dnsMonitoringInterval: 5000
#dnsMonitoring: false
threads: 0
nettyThreads: 0
codec:
class: "org.redisson.codec.JsonJacksonCodec"
transportMode: "NIO"
1.编写类RedissonConfig,将redisson的核心类RedissonClient交给spring管理。
package com.cmdc.config;
import com.cmdc.lockimpl.RedissonDistributeLocker;
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;
import java.io.IOException;
/**
* redisson bean管理
*/
@Configuration
public class RedissonConfig {
/**
* Redisson客户端注册
* 单机模式
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient createRedissonClient() throws IOException {
// Config config = new Config();
// SingleServerConfig singleServerConfig = config.useSingleServer();
// singleServerConfig.setAddress("redis://127.0.0.1:6379");
// singleServerConfig.setPassword("12345");
// singleServerConfig.setTimeout(3000);
// return Redisson.create(config)
// 本例子使用的是yaml格式的配置文件,读取使用Config.fromYAML,如果是Json文件,则使用Config.fromJSON
Config config = Config.fromYAML(RedissonConfig.class.getClassLoader().getResource("redisson-config.yml"));
return Redisson.create(config);
}
/**
* 主从模式 哨兵模式
*
**/
/* @Bean
public RedissonClient getRedisson() {
RedissonClient redisson;
Config config = new Config();
config.useMasterSlaveServers()
//可以用"rediss://"来启用SSL连接
.setMasterAddress("redis://***(主服务器IP):6379").setPassword("web2017")
.addSlaveAddress("redis://***(从服务器IP):6379")
.setReconnectionTimeout(10000)
.setRetryInterval(5000)
.setTimeout(10000)
.setConnectTimeout(10000);//(连接超时,单位:毫秒 默认值:3000);
// 哨兵模式config.useSentinelServers().setMasterName("mymaster").setPassword("web2017").addSentinelAddress("***(哨兵IP):26379", "***(哨兵IP):26379", "***(哨兵IP):26380");
redisson = Redisson.create(config);
return redisson;
}*/
}
编写类DistributeLocker,这是一个接口。用来操作redisson提供给我们的核心类RedissonClient。
package com.cmdc.abstractlock;
import java.util.concurrent.TimeUnit;
/**
*
*/
public interface DistributeLocker {
/**
* 加锁
* @param lockKey key
*/
void lock(String lockKey);
/**
* 加锁锁,设置有效期
*
* @param lockKey key
* @param timeout 有效时间,默认时间单位在实现类传入
*/
void lock(String lockKey, int timeout);
/**
* 加锁,设置有效期并指定时间单位
* @param lockKey key
* @param timeout 有效时间
* @param unit 时间单位
*/
void lock(String lockKey, int timeout, TimeUnit unit);
/**
* 释放锁
*
* @param lockKey key
*/
void unlock(String lockKey);
/**
* 尝试获取锁,获取到则持有该锁返回true,未获取到立即返回false
* @param lockKey 锁
* @return true-获取锁成功 false-获取锁失败
*/
boolean tryLock(String lockKey);
/**
* 尝试获取锁,获取到则持有该锁leaseTime时间.
* 若未获取到,在waitTime时间内一直尝试获取,超过waitTime还未获取到则返回false
* @param lockKey key
* @param waitTime 尝试获取时间
* @param leaseTime 锁持有时间
* @param unit 时间单位
* @return true-获取锁成功 false-获取锁失败
* @throws InterruptedException e
*/
boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
throws InterruptedException;
/**
* 锁是否被任意一个线程锁持有
* @param lockKey 锁
* @return true-被锁 false-未被锁
*/
boolean isLocked(String lockKey);
/**
* isHeldByCurrentThread()的作用是查询当前线程是否保持此锁定
* @param lockKey 锁
* @return true or false
*/
boolean isHeldByCurrentThread(String lockKey);
}
下面将其实现,编写DistributeLocker的实现类RedissonDistributeLocker,将上述方法全部实现,方法的作用接口类中已经描述的比较清楚了。咱们这把不研究API,感兴趣的童鞋自己看看鸭
package com.cmdc.lockimpl;
import com.cmdc.abstractlock.DistributeLocker;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
/**
* redisson实现分布式锁接口
*/
public class RedissonDistributeLocker implements DistributeLocker {
private final RedissonClient redissonClient;
/**
* 构造方法 赋予本类的redisClient以实例
* @param redissonClient client
*/
public RedissonDistributeLocker(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Override
public void lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
}
@Override
public void lock(String lockKey, int leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime, TimeUnit.MILLISECONDS);
}
@Override
public void lock(String lockKey, int timeout, TimeUnit unit) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
public boolean tryLock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.tryLock();
}
@Override
public boolean tryLock(String lockKey, long waitTime, long leaseTime,
TimeUnit unit) throws InterruptedException {
RLock lock = redissonClient.getLock(lockKey);
return lock.tryLock(waitTime, leaseTime, unit);
}
@Override
public boolean isLocked(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.isLocked();
}
@Override
public boolean isHeldByCurrentThread(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.isHeldByCurrentThread();
}
}
ok,将RedissonDistributeLocker交给spring进行管理。别问,问就是习惯了~
这个需要在之前的配置类RedissonConfig中增加一个注入的方法。
@Bean
public RedissonDistributeLocker redissonLocker(RedissonClient redissonClient) {
// redissonClient 是本来就由redisson提供给我们,我们创建RedissonDistributeLocker实例交给spring进行管理
RedissonDistributeLocker locker = new RedissonDistributeLocker(redissonClient);
return locker;
}
继续,增加一个注解RedissonLockAnnotation用于标记需要用上分布式锁的方法以及提供表示锁的字符串
package com.cmdc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 分布式锁自定义注解
* 注解在方法
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLockAnnotation {
/**
* 指定组成分布式锁的key
* @return 分布式锁的key
*/
String lockRedisKey();
}
将这个注解AOP增强,赋予它生命
package com.cmdc.annotationimpl;
import com.cmdc.abstractlock.DistributeLocker;
import com.cmdc.annotation.RedissonLockAnnotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁的 aop
*
* 无论是否抛出异常,也无论从什么地方return返回,finally语句块总是会执行,这样你有机会调用Close来关闭数据库连接(即使未打开或打开失败,关闭操作永远是可以执行的),以便于释放已经产生的连接,释放资源。
*
* 顺便说明,return是可以放在try语句块中的。但不管在什么时机返回,在返回前,finally将会执行。
* 小结:
* try { //执行的代码,其中可能有异常。一旦发现异常,则立即跳到catch执行。否则不会执行catch里面的内容 }
* catch { //除非try里面执行代码发生了异常,否则这里的代码不会执行 }
* finally { //不管什么情况都会执行,包括try catch 里面用了return ,可以理解为只要执行了try或者catch,就一定会执行 finally }
*
* Case2:
* 至少有两种情况下finally语句是不会被执行的:
* (1)try语句没有被执行到,如在try语句之前return就返回了,这样finally语句就不会执行。这也说明了finally语句被执行的必要而非充分条件是:相应的try语句一定被执行到。
* (2)在try块|catch块中有System.exit(0);这样的语句。System.exit(0)是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。
*
* 在try-catch-finally中, 当return遇到finally,return对finally无效,即:
*
* 1.在try catch块里return的时候,finally也会被执行。
*
* 2.finally里的return语句会把try catch块里的return语句效果给覆盖掉。
*
* 结论:return语句并不一定都是函数的出口,执行return时,只是把return后面的值复制了一份到返回值变量里去了。
*/
@Aspect
@Component
@Slf4j
public class RedissonLockAop {
public static final int WAIT_GET_LOCK_TIME = 3000;
public static final int WAIT_RELEASE_LOCK_TIME = 5000;
@Autowired
private DistributeLocker locker;
/**
* 切点,拦截被 @RedissonLockAnnotation 修饰的方法
* 说白了就是你这把面向切面从哪里切
*/
@Pointcut("@annotation(com.cmdc.annotation.RedissonLockAnnotation)")
public void redissonLockPoint() {
}
/**
*
* @param pjp 代表当前正在运行的方法
* @return string
* @throws InterruptedException e
*/
@Around("redissonLockPoint()")
@ResponseBody
public String checkLock(ProceedingJoinPoint pjp) throws InterruptedException {
// 当前线程名
String threadName = Thread.currentThread().getName();
log.info("线程{}------进入分布式锁aop------", threadName);
// 获取该注解的实例对象
RedissonLockAnnotation annotation = ((MethodSignature) pjp.getSignature()).
getMethod().getAnnotation(RedissonLockAnnotation.class);
// 生成分布式锁key的键名,以逗号分隔
String lockRedisKey = annotation.lockRedisKey();
log.info("存在于注解中的key值是:{}",lockRedisKey);
// 获取存在于请求头中的唯一id值
String lockRedisValue = ((ServletRequestAttributes) Objects.requireNonNull(
RequestContextHolder.getRequestAttributes()))
.getRequest()
.getHeader(lockRedisKey);
if (StringUtils.isEmpty(lockRedisKey)) {
log.info("线程{} lockRedisKey设置为空,不加锁", threadName);
try {
pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
log.info("process method failed...now print message:{}",throwable.getMessage());
}
return "NULL LOCK";
} else {
log.info("线程{} 锁的value值是:{}", threadName, lockRedisValue);
// 获取锁 3000 等到获取锁的时间 leaseTime 获取锁后持有时间 时间单位 MILLISECONDS:毫秒
if (locker.tryLock(lockRedisValue, WAIT_GET_LOCK_TIME, WAIT_RELEASE_LOCK_TIME, TimeUnit.MILLISECONDS)) {
// 下面的逻辑我想说一下,大家应该非常清楚try catch finally的逻辑 关于这一块的逻辑已经卸载顶层注释上了
try {
log.info("线程{} 获取锁成功", threadName);
return (String) pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
log.info("process method failed...now print message:{}",throwable.getMessage());
} finally {
if (locker.isLocked(lockRedisValue)) {
log.info("key={}对应的锁被持有,线程{}",lockRedisValue, threadName);
if (locker.isHeldByCurrentThread(lockRedisValue)) {
log.info("当前线程 {} 保持锁定", threadName);
locker.unlock(lockRedisValue);
log.info("线程{} 释放锁", threadName);
}
}
}
} else {
log.info("线程{} 获取锁失败", threadName);
return " GET LOCK FAIL";
}
}
return null;
}
}
这边稍微说一下,增强的思想就是,需要分布式锁的接口有多个线程进来的时候,每个线程都在请求头中放置一个唯一的id,这个唯一的id就抽象地对应一个共享变量。利用redis set值是一个原子操作,让线程去set 这个唯一的id,完成这个set的线程才能执行业务处理,不能完成的则不能进行业务的处理。当持有锁的线程完成业务处理之后即释放锁,以此循环往复。具体的low一眼代码也就明白啦。
下面咱们弄一个接口出来执行测试~
package com.cmdc.controller;
import com.cmdc.annotation.RedissonLockAnnotation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
*/
@RestController
@Slf4j
public class TestController {
public static final int THREAD_SLEEP_TIME = 5000;
/**
* 测试接口
* @return 返回值
*/
@PostMapping(value = "testLock", consumes = "application/json")
@RedissonLockAnnotation(lockRedisKey = "the-only-id")
public String testLock() {
/**
* 请求总携带一个唯一的id 谁拿到谁执行,非常的好理解
*/
try {
Thread.sleep(THREAD_SLEEP_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("执行相关业务...");
log.info("业务执行中.....");
log.info("业务执行结束.....");
return "success";
}
}
沉睡5秒钟模拟业务的处理,咱们同时发送两个请求,看看会发生什么,我有postman和Insomnia两个测试的工具,就分别发请求,如果你只有一个工具,就一个请求复制两份就可以了
先发postman,看测试结果
接着是Insomnia来看测试的结果
Insomnia后发的,未成功获取到锁,和我们的预期一致的!
线程池是为了支持并发、管控资源。
分布式锁是为了限制并发、解决线程安全问题,这两个都非常深刻,我们慢慢体会。
其实不一定需要用redisson,纯redis实现分布式锁也一点问题没有!我这里只是不想自己造轮子了~
不用redis用zookeeper也行,都没问题,看你喜欢什么啦!