微信公众号:[执着猿哥]
记录和分享java、springcloud等企业级编码技术知识。有问题或建议和源码,请关注公众号。
我们在进行springboot项目开发的时候,经常会引入官方或者第三方的组件的,比如 redisson官方的“redisson-spring-boot-starter”、mybatis的"mybatis-spring-boot-starter" 只要依赖下,进行简单的配置就可以立马使用的了。而我们做的组件也被依赖,但是要启用各种配置和扫描路径等等才能用。本编通过实现分布式锁的springboot Starters 组件,一步一步实现自己的Spring Boot Starters。
在集群架构中,重复提交是一个常见问题,项目中常常会遇到这种状况,造成的影响也很大。在高并发下,像秒杀,抢票,抢购商品的场景,都存在对核心资源,商品库存的争夺,把控不好,会出现超卖的情况。如果网速比较慢的情况下,用户提交表单后发现没有响应,再次点击提交表单,或者因为其他系统引起的,比如:
上面介绍也就是的接口幂等性需求了。
在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。所以为了避免上面的问题,接引入接口幂等性。但是引入接口幂等性又对系统造成了影响,
主要体现:
所以在使用时候需考虑是否引入幂等性的必要性,根据实际业务场景具体分析,现流行的 Restful 推荐的几种 HTTP 接口方法中,除非业务特殊要求,一般情况下,不需要引入接口幂等性。
方法类型 | 需要幂等 | 描述 |
---|---|---|
Get | × | get获取资源请求,不影响资源,自带幂等 |
Post | √ | post提交新资源。其每次执行都会新增数据,所以不是幂等的。 |
Put | √ | put 修改资源。该操作根据某个值进行更新,也能保持幂等,但如果对某个特定资源比如商品抢购就必须带有幂等性要求 |
Delete | × | delete 方法一般用于删除资源。根据主键或者相关值进行不需要幂等性,加强异常判断就可满足 |
目前常见的方案有:数据库唯一主键、数据库乐观锁、防重 Token 令牌、分布式锁等。本篇使用redis官方推荐的Redisson来方式实现分布式。
Redisson满足三大原则,更提供了重入锁机制:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用,自带了续锁功能(watch dog)。Redission分布式锁建议redis版本在redis5或者以上版本使用集群模式。
默认规则:SpringBoot提供的starter以spring-boot-starter-xxx的方式命名的。官方建议自定义的starter使用 xxx-spring-boot-starter命名规则。以区分SpringBoot生态提供的starter。比如:mybatis-spring-boot-starter、mybatisplus-spring-boot-starter等等。实现分布式锁参考了 Lock4j 。
有了以上了解,工程目录如下: toolset 属于分类工程,lock-redisson-spring-boot-starter子工程就是要实现的分布式锁starter组件
<parent>
<artifactId>toolsetartifactId>
<groupId>com.zzyge.toolsetgroupId>
<version>0.0.1version>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>toolset-lockredisson-spring-boot-starterartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>${redisson.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
工程采用父子项目结构,把工具类全部封装在toolset父目录中,统一管理和版本。
<dependencies>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>${redisson.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
dependencies>
/**
* 锁参数配置
*
*/
//@ConfigurationProperties 前缀 不能有大写
@ConfigurationProperties(prefix = "lockredisson")
public class LockRedissonProperties {
/**
* 过期时间 =单位:毫秒,
*/
private long expire = 3000;
/**
* 获取锁超时时间= 单位:毫秒,,,默认30秒
*/
private long getLockTimeout = 30000;
/**
* 获取锁失败时重试时间间隔 =单位:毫秒,
*/
private long retryTimeFailure = 100;
/**
* 重试次数
*
* @return
*/
private int retryTimes = 5;
/**
* 在yal参数配置的前缀
*/
private String lockKeyPrefix = "lockredisson";
/******余下**set get方法*********/
}
在自定义Spring Starter时通常可以在application.yml中来配置参数覆盖掉默认的值。即expire、getLockTimeout等的值会被配置文件中的值替换掉。
//@SuppressWarnings 忽略所有的红线
@SuppressWarnings("unused")
@Configuration
//(只有Redisson的class位于类路径上,才会实例化一个RedissonLockConfiguration baen)
@ConditionalOnClass(Redisson.class)
@EnableConfigura//@EnableConfigurationProperties通常是用来将properties和yml配置文件属性转化为bean对象使用
tionProperties(LockRedissonProperties.class)
public class LockRedissonAopConfiguartion {
private final LockRedissonProperties lockRedissonProperties;
/**
* 接口自动化加载报错
*/
// private final ILockRedissonFailure lockRedissonFailure;
/**
* 自动加载,设置lockRedissonProperties参数
*
* @param lockRedissonProperties
*/
public LockRedissonAopConfiguartion(LockRedissonProperties lockRedissonProperties) {
this.lockRedissonProperties = lockRedissonProperties;
}
/**
* @ConditionalOnMissingBean注解作用在@bean定义上,它的作用就是在容器加载它作用的bean时,
* 检查容器中是否存在目标类型(ConditionalOnMissingBean注解的value值)
* 的bean了,如果存在这跳过原始bean的BeanDefinition加载动作。
*
*/
@Bean
@ConditionalOnMissingBean
public LockRedissonAop lockRedissonAop(ILockRedissonFailure lockRedissonFailure, ILockRedissonKey lockRedissonKey) {
return new LockRedissonAop(lockRedissonProperties, lockRedissonFailure, lockRedissonKey);
}
/**
* 配置一个默认实现 ILockRedissonFailure类,默认加载到springboot中
*/
@Bean
@ConditionalOnMissingBean
public ILockRedissonFailure lockRedissonFailure() {
return new DefaultLockRedissonFailure();
}
/**
* 配置一个默认实现 生成key的类
*/
@Bean
@ConditionalOnMissingBean
public ILockRedissonKey lockRedissonKey() {
return new DefaultLockRedissonKey();
}
/**
* 加载模版类,用来操作锁定相关方法
*
* @return
*/
@Bean
@ConditionalOnMissingBean
public LockRedissonTemplate lockRedissonTemplate() {
return new LockRedissonTemplate(lockRedissonProperties);
}
/***
* 加载LockRedissonAction关于Redisson锁的操作方法
*
* @param redissonClient
* @return
*/
@Bean
public LockRedissonAction redissonLockExecutor(RedissonClient redissonClient) {
return new LockRedissonAction(redissonClient);
}
}
LockRedissonAopConfiguartion这个类就是把实现关于分布式锁所有的对象new的地方,并提交给springboot来管理所有的bean创建和使用。总不能让使用人用@ComponentScan去扫码我们的组件baen。
在工程中/src/main/resources目录中创建 “META-INF” 文件夹,“META-INF” 文件夹 中创建 “spring.factories文件。在在“spring.factories文件中配置自己的自动配置类。如果有多跟按, 分隔。\ 是换行符
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zzyge.toolset.lockredisson.boot.LockRedissonAopConfiguartion
public interface ILockAction<T> {
/**
* 获取可重入锁 ReentrantLock
*
* @param lockKey 锁标识
* @param expire 锁有效时间
* @param acquireTimeout 获取锁超时时间
* @return 锁信息
*/
T getLock(String lockKey, long expire, long getLockTimeout);
/**
* 释放获到的锁
*
* @param key 锁key
* @param value 锁value
* @param lockInstance 锁实例
* @return 是否释放成功
*/
boolean releaseLock(String lockKey, T lockInstance);
}
7、定义锁操作回调接口实现类和模版类
public class LockRedissonAction extends AbstractLockAction {
private final RedissonClient redissonClient;
/**
* 自动加载bean
*
* @param redissonClient
*/
public LockRedissonAction(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Override
public RLock getLock(String lockKey, long expire, long getLockTimeout) {
try {
// 获取普通的可重入锁
RLock lock = redissonClient.getLock(lockKey);
// 拿锁失败时会不停的重试
// 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
// lock.lock();
// 拿锁失败时会不停的重试
// 没有Watch Dog ,10s后自动释放
// lock.lock(10, TimeUnit.SECONDS);
// 尝试拿锁10s后停止重试,返回false
// 具有Watch Dog 自动延期机制 默认续30s
// boolean res1 = lock.tryLock(10, TimeUnit.SECONDS);
// 尝试拿锁100s后停止重试,返回false
// 没有Watch Dog ,10s后自动释放
// boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);
// 秒
boolean isFlage = lock.tryLock(getLockTimeout, expire, TimeUnit.MILLISECONDS);
return isFlage ? lock : null;
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
@Override
public boolean releaseLock(String lockKey, RLock lockInstance) {
// 检查该锁是否被当前线程持有
if (lockInstance.isHeldByCurrentThread()) {
try {
return lockInstance.forceUnlockAsync().get();
} catch (ExecutionException | InterruptedException e) {
return false;
}
}
return false;
}
public RedissonClient getRedissonClient() {
return redissonClient;
}
}
模版类:
public class LockRedissonTemplate {
private final Logger logger = LoggerFactory.getLogger(LockRedissonTemplate.class);
@Autowired
private LockRedissonAction lockRedissonAction;
private final LockRedissonProperties lockRedissonProperties;
/***
* 自动加载,
*
* @param lockRedissonAction
* @param lockRedissonProperties
*/
public LockRedissonTemplate(LockRedissonProperties lockRedissonProperties) {
super();
this.lockRedissonProperties = lockRedissonProperties;
}
/***
*
* @param lockKey 锁key
* @param expire 获取锁后的过期时间,如果是0,走配置的值
* @param getLockTimeout 获取锁超时时间,如果是0,走配置的值
* @return
*/
public LockRedissonInfo getLock(String lockKey, long expire, long getLockTimeout) {
// 如果是注解类上的配置优先配置文件
// 超时时间
long expire1 = expire > 0 ? expire : lockRedissonProperties.getExpire();
// 获取锁超时时间
long getLockTimeout1 = getLockTimeout > 0 ? getLockTimeout : lockRedissonProperties.getGetLockTimeout();
// 获取锁失败时重试时间间隔
long retryTimeFailure = lockRedissonProperties.getRetryTimeFailure();
// 重试次数
long retryTimes = lockRedissonProperties.getRetryTimes();
long retryTimes1 = 0;
try {
do {
retryTimes1++;
RLock lockRLock = lockRedissonAction.getLock(lockKey, expire1, getLockTimeout1);
if (lockRLock != null) {
logger.info("====================获取锁成功=========key===========" + lockKey);
return new LockRedissonInfo(lockKey, lockRLock, expire1, getLockTimeout1);
}
logger.info("====================获取锁失败,正在重试获取===key===========" + lockKey);
// 获取锁失败,延迟再试
TimeUnit.MILLISECONDS.sleep(retryTimeFailure);
} while (retryTimes1 <= retryTimes);
} catch (Exception e) {
throw new LockRedissonException(ExceptionUtils.getStackTraceInfo(e));
}
return null;
}
/*************** 释放锁 *******************************/
public boolean releaseLock(LockRedissonInfo lockRedissonInfo) {
return lockRedissonAction.releaseLock(lockRedissonInfo.getLockKey(), lockRedissonInfo.getLockRLock());
}
}
8、获取锁失败的回调接口和实现类
public interface ILockRedissonFailure {
/**
* 锁失败
*/
void onLockFailure(String key, Method method, Object[] arguments);
}
public class DefaultLockRedissonFailure implements ILockRedissonFailure {
@Override
public void onLockFailure(String key, Method method, Object[] arguments) {
StringBuffer sb = new StringBuffer();
sb.append("请求分布式接口失败,请重试:");
sb.append("key=");
sb.append(key);
sb.append("#包和方法名=");
sb.append(method.getDeclaringClass().getName());
sb.append(method.getName());
throw new LockRedissonFailureException(sb.toString());
}
}
/**
*
* key 生成接口类
*/
public interface ILockRedissonKey {
/**
* 构建key
*
* @param Method method 方法参数
* @param lockRedissonKeys LockRedisson注解上的keys
* @return 生成锁key
*/
Object buildKey(ProceedingJoinPoint joinPoint, String[] lockRedissonKeys);
}
实现类
/**
*key生产默认实现类
*/
public class DefaultLockRedissonKey implements ILockRedissonKey {
private final Logger logger = LoggerFactory.getLogger(DefaultLockRedissonKey.class);
private final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private final ExpressionParser PARSER = new SpelExpressionParser();
@Override
public Object buildKey(ProceedingJoinPoint joinPoint, String[] lockRedissonKeys) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
if (lockRedissonKeys.length > 1 || !"".equals(lockRedissonKeys[0])) {
return getSpelDefinitionKey(lockRedissonKeys, signature.getMethod(), joinPoint.getArgs());
}
return null;
}
protected Long getSpelDefinitionKey(String[] definitionKeys, Method method, Object[] parameterValues) {
EvaluationContext context = new MethodBasedEvaluationContext(null, method, parameterValues, NAME_DISCOVERER);
List<String> definitionKeyList = new ArrayList<>(definitionKeys.length);
for (String definitionKey : definitionKeys) {
if (definitionKey != null && !definitionKey.isEmpty()) {
String key = PARSER.parseExpression(definitionKey).getValue(context, String.class);
definitionKeyList.add(key);
}
}
String paramS = StringUtils.collectionToDelimitedString(definitionKeyList, ".", "", "");
//logger.info("=======获取方法参数,生成锁key的======" + paramS);
//防止key值太长,用根据其生成的hash值做key
long hasString = HashUtil.hfHash(paramS);
//logger.info("=======生成锁key的hash后的值======" + hasString);
return hasString;
}
}
10、使用AOP 简化分布式锁
定义注解@LockRedisson
@Target(value = { ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface LockRedisson {
/**
* 用于多个方法锁同一把锁 可以理解为锁资源名称 为空则会使用 包名+类名+方法名,类似锁的资源分组
*
* @return 名称
*/
String lockName() default "";
/**
* support SPEL expresion 锁的key = name + keys
*
* @return KEY
*/
String[] keys() default "";
/**
* @return 过期时间 单位:毫秒
*
* 过期时间一定是要长于业务的执行时间. 未设置则为默认时间30秒
* 默认值:{@link LockRedissonProperties#expire}
*/
long expire() default 30000;
/**
* @return 获取锁超时时间 单位:毫秒
*
*
* 结合业务,建议该时间不宜设置过长,特别在并发高的情况下. 未设置则为默认时间3秒 默认值:{@link LockRedissonProperties#getLockTimeout}
*
*/
long getLockTimeout() default 3000;
/**
* 获取锁失败时重试时间间隔 =单位:毫秒
*
* @return
*/
long retryTimeFailure() default 100;
/**
* 重试次数
*
* @return
*/
int retryTimes() default 5;
}
定义切面织入的代码LockRedissonAop
@Aspect
@ConditionalOnClass(LockRedisson.class)
//@Component
public class LockRedissonAop {
private final Logger logger = LoggerFactory.getLogger(LockRedissonAop.class);
@Autowired
private LockRedissonAction lockRedissonAction;
private final LockRedissonProperties lockRedissonProperties;
private final ILockRedissonFailure lockRedissonFailure;
private final ILockRedissonKey lockRedissonKey;
/**
* 自动加载配置
*
* @param lockRedissonProperties
*/
public LockRedissonAop(@NonNull LockRedissonProperties lockRedissonProperties,
@NonNull ILockRedissonFailure lockRedissonFailure,
@NonNull ILockRedissonKey lockRedissonKey) {
this.lockRedissonProperties = lockRedissonProperties;
this.lockRedissonFailure = lockRedissonFailure;
this.lockRedissonKey = lockRedissonKey;
}
@Pointcut("@annotation(com.zzyge.toolset.lockredisson.annotation.LockRedisson)")
public void lockRedissonMethon() {
}
@Around("lockRedissonMethon()")
public Object aroundMethon(ProceedingJoinPoint proceedingJoinPoint) {
logger.info("===============lockRedissonMethon 进入分布式锁aop=========================");
LockRedissonInfo lockRedissonInfo = null;
try {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
LockRedisson lockRedisson = AnnotationUtils.findAnnotation(signature.getMethod(), LockRedisson.class);
// 分布式锁
if (lockRedisson != null) {
//
logger.info("====================锁key生成=====================");
StringBuffer sb = new StringBuffer();
// 前缀
sb.append(lockRedissonProperties.getLockKeyPrefix());
sb.append(":");
// 锁的资源分组,资源分组
// 如果lockRedisson.lockName()为空,默认包名+类名+方法名
String packageName = signature.getMethod().getDeclaringClass().getName();
String methodName = signature.getMethod().getName();
if (StringUtils.isEmpty(lockRedisson.lockName())) {
// 如果是空的
sb.append(packageName);// 包名
sb.append(".");// 包名
sb.append(methodName);// 方法名
} else {
// 如果不是空的
sb.append(lockRedisson.lockName());// 自定义锁资源分组
}
// SPEL表达式
Object lockKey = lockRedissonKey.buildKey(proceedingJoinPoint, lockRedisson.keys());
if (!StringUtils.isEmpty(lockKey)) {
sb.append("#");
sb.append(lockKey);
}
logger.info("====================分布式锁key====================" + sb.toString());
lockRedissonInfo = AopUitls.getLock(lockRedissonAction, lockRedissonProperties, lockRedisson,
sb.toString());
if (lockRedissonInfo != null) {
return proceedingJoinPoint.proceed();
}
// 如果失败
lockRedissonFailure.onLockFailure(sb.toString(), signature.getMethod(), proceedingJoinPoint.getArgs());
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
if (lockRedissonInfo != null) {
AopUitls.releaseLock(lockRedissonAction, lockRedissonInfo);
logger.info("===================分布式锁释放成功" + lockRedissonInfo.getLockKey());
}
}
return null;
}
}
在lock-redisson-spring-boot-starter工程中执行mvn clean install ,一个自定义的starter就完成了。注意目前是保存到本地,还不能给他人使用。
新建SpringBoot测试工程
引入starter依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>com.zzyge.toolsetgroupId>
<artifactId>toolset-lockredisson-spring-boot-starterartifactId>
<version>0.0.1version>
dependency>
dependencies>
配置文件
spring:
redis:
host: 175.24.172.190
port: 63188
password: 123456&
lockredisson:
#过期时间 =单位:毫秒
expire: 30000
# 获取锁超时时间= 单位:毫秒
getLockTimeout: 3000
# 获取锁失败时重试时间间隔 =单位:毫秒
retryTimeFailure: 100
#获取锁失败重试次数
retryTimes: 5
然后写个测试类
@SpringBootTest(classes = ToolSerTest.class)
//不能去掉,不能跑不起来
@SpringBootApplication
@RunWith(SpringJUnit4ClassRunner.class) // 让测试运行于Spring测试环境
public class ToolSerTest {
public int count = 1;
@Autowired
TestService testService;
@Autowired
LockRedissonTemplate lockRedissonTemplate;
@Test
public void tesst1() {
testService.test1();
}
/****
* 验证keys SPEL表达
*/
@Test
public void tesst2() {
UserInfo userInfo = new UserInfo();
userInfo.setId(98008388119L);
userInfo.setAge(12);
userInfo.setName("测试君");
testService.test2(userInfo);
}
/****
* 验证锁被占用情况
*/
@Test
public void tesst3() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Runnable task = new Runnable() {
@Override
public void run() {
try {
testService.test1();
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 15; i++) {
executorService.submit(task);
}
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
// executorService.shutdown();
}
/****
* 验证锁被占用情况
*/
@Test
public void tesst4() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Runnable task = new Runnable() {
@Override
public void run() {
try {
UserInfo userInfo = new UserInfo();
userInfo.setId(9800838811l);
userInfo.setAge(12);
userInfo.setName("测试君");
testService.test3(userInfo);
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 15; i++) {
executorService.submit(task);
}
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/****
* 测试手动上锁和释放锁
*/
@Test
public void tesst5() {
// 获取锁
final LockRedissonInfo lockInfo = lockRedissonTemplate.getLock("11111111111111411e", 30000L, 5000L);
if (null == lockInfo) {
throw new RuntimeException("获取锁失败,请重试");
}
// 获取锁成功,处理业务
try {
System.out.println("执行简单方法1 , 当前线程:" + Thread.currentThread().getName() + " , count:" + (count++));
} finally {
// 释放锁
System.out.println("手动释放锁成功");
lockRedissonTemplate.releaseLock(lockInfo);
}
// 结束
}
/****
* 自定义生成key
*/
@Test
public void tesst6() {
UserInfo userInfo = new UserInfo();
userInfo.setId(9800838811l);
userInfo.setAge(12);
userInfo.setName("测试君");
testService.test2(userInfo);
}
验证效果: