微信公众号:[执着猿哥] 记录和分享java、springcloud等企业级编码技术知识。有问题或建议和源码,请关注公众号。
自定义属于自己的Spring Boot Starters
? 我们在进行springboot项目开发的时候,经常会引入官方或者第三方的组件的,比如 redisson官方的“redisson-spring-boot-starter”、mybatis的"mybatis-spring-boot-starter" 只要依赖下,进行简单的配置就可以立马使用的了。而我们做的组件也被依赖,但是要启用各种配置和扫描路径等等才能用。本编通过实现分布式锁的springboot Starters 组件,一步一步实现自己的Spring Boot Starters。
场景分析
在集群架构中,重复提交是一个常见问题,项目中常常会遇到这种状况,造成的影响也很大。在高并发下,像秒杀,抢票,抢购商品的场景,都存在对核心资源,商品库存的争夺,把控不好,会出现超卖的情况。如果网速比较慢的情况下,用户提交表单后发现没有响应,再次点击提交表单,或者因为其他系统引起的,比如:
- 接口重复请求:网络环境抖动引发的nginx重发请求,造成重复调用。
- 用户恶意重复点击:比如秒杀商品,秒杀工具恶意重复提交等。
- 接口超时重复提交:很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
- 消息进行重复消费:*当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。
? 上面介绍也就是的接口幂等性需求了。
什么是接口幂等性
? 在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。所以为了避免上面的问题,接引入接口幂等性。但是引入接口幂等性又对系统造成了影响, 主要体现:
- 把并行执行的功能改为串行执行,降低了执行效率。
- 增加了额外控制幂等的业务逻辑,复杂化了业务功能;
? 所以在使用时候需考虑是否引入幂等性的必要性,根据实际业务场景具体分析,现流行的 Restful 推荐的几种 HTTP 接口方法中,除非业务特殊要求,一般情况下,不需要引入接口幂等性。
方法类型 | 需要幂等 | 描述 |
---|
Get | × | get获取资源请求,不影响资源,自带幂等 | Post | √ | post提交新资源。其每次执行都会新增数据,所以不是幂等的。 | Put | √ | put 修改资源。该操作根据某个值进行更新,也能保持幂等,但如果对某个特定资源比如商品抢购就必须带有幂等性要求 | Delete | × | delete 方法一般用于删除资源。根据主键或者相关值进行不需要幂等性,加强异常判断就可满足 |
实现幂等性方案
目前常见的方案有:数据库唯一主键、数据库乐观锁、防重 Token 令牌、分布式锁等。本篇使用redis官方推荐的Redisson来方式实现分布式。
分布式锁应遵循的原则
- 互斥性:任意时刻,同一个锁,只有一个进程能持有
- 安全性:避免死锁,当进程没有主动释放锁(进程崩溃退出),保证其他进程能够加锁.
- 容错性:高可用,也就是生产锁和消灭锁的的服务应该支持高可用,借助第三方比如redis实现
? Redisson满足三大原则,更提供了重入锁机制:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用,自带了续锁功能(watch dog)。Redission分布式锁建议redis版本在redis5或者以上版本使用集群模式。
Spring Boot Starters 分布式锁实现
自定义starter的命名规则
? 默认规则: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组件
1、添加pom.xml依赖(根据组件功能添加)
<parent>
<artifactId>toolset</artifactId>
<groupId>com.zzyge.toolset</groupId>
<version>0.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>toolset-lockredisson-spring-boot-starter</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies
? 工程采用父子项目结构,把工具类全部封装在toolset父目录中,统一管理和版本。
2、添加pom.xml依赖(根据组件功能添加)
<dependencies>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
3、LockRedissonProperties.java——属性配置类,这个类是Starters需要的配置东西。
@ConfigurationProperties(prefix = "lockredisson")
public class LockRedissonProperties {
private long expire = 3000;
private long getLockTimeout = 30000;
private long retryTimeFailure = 100;
private int retryTimes = 5;
private String lockKeyPrefix = "lockredisson";
}
在自定义Spring Starter时通常可以在application.yml中来配置参数覆盖掉默认的值。即expire、getLockTimeout等的值会被配置文件中的值替换掉。
4、LockRedissonAopConfiguartion.java类 引用定义好的配置信息,实现所有starter应该完成的操作,并且把这个类加入spring.factories配置文件中进行声明
//@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。
5、新增spring.factories文件,让springboot管理组件Bean
在工程中/src/main/resources目录中创建 “META-INF” 文件夹,“META-INF” 文件夹 中创建 “spring.factories文件。在在“spring.factories文件中配置自己的自动配置类。如果有多跟按, 分隔。\ 是换行符
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zzyge.toolset.lockredisson.boot.LockRedissonAopConfiguartion
6、定义锁操作回调接口
public interface ILockAction<T> {
T getLock(String lockKey, long expire, long getLockTimeout);
boolean releaseLock(String lockKey, T lockInstance);
}
7、定义锁操作回调接口实现类和模版类
public class LockRedissonAction extends AbstractLockAction<RLock> {
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;
public LockRedissonTemplate(LockRedissonProperties lockRedissonProperties) {
super();
this.lockRedissonProperties = lockRedissonProperties;
}
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());
}
}
9、分布式key生产接口类以及实现类
/**
*
* key 生成接口类
*/
public interface ILockRedissonKey {
/**
* 构建key
*
* @param Method method 方法参数
* @param lockRedissonKeys LockRedisson注解上的keys
* @return 生成锁key
*/
Object buildKey(ProceedingJoinPoint joinPoint, String[] lockRedissonKeys);
}
实现类
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, ".", "", "");
long hasString = HashUtil.hfHash(paramS);
return hasString;
}
}
10、使用AOP 简化分布式锁
定义注解@LockRedisson
@Target(value = { ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface LockRedisson {
String lockName() default "";
String[] keys() default "";
long expire() default 30000;
long getLockTimeout() default 3000;
long retryTimeFailure() default 100;
int retryTimes() default 5;
}
定义切面织入的代码LockRedissonAop
@Aspect
@ConditionalOnClass(LockRedisson.class)
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;
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(":");
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());
}
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.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.zzyge.toolset</groupId>
<artifactId>toolset-lockredisson-spring-boot-starter</artifactId>
<version>0.0.1</version>
</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)
public class ToolSerTest {
public int count = 1;
@Autowired
TestService testService;
@Autowired
LockRedissonTemplate lockRedissonTemplate;
@Test
public void tesst1() {
testService.test1();
}
@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();
}
}
@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);
}
}
@Test
public void tesst6() {
UserInfo userInfo = new UserInfo();
userInfo.setId(9800838811l);
userInfo.setAge(12);
userInfo.setName("测试君");
testService.test2(userInfo);
}
验证效果:
好了可以上传私服,提供给别人移入吧。
|