mirror of
https://github.com/zongzibinbin/MallChat.git
synced 2026-03-17 08:13:42 +08:00
feat:
1.搭建分布式事务框架 2.项目引入rocketmq实现集群广播
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package com.abin.mallchat.transaction.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 保证方法成功执行。如果在事务内的方法,会将操作记录入库,保证执行。
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)//运行时生效
|
||||
@Target(ElementType.METHOD)//作用在方法上
|
||||
public @interface SecureInvoke {
|
||||
|
||||
/**
|
||||
* 默认3次
|
||||
*
|
||||
* @return 最大重试次数(包括第一次正常执行)
|
||||
*/
|
||||
int maxRetryTimes() default 3;
|
||||
|
||||
/**
|
||||
* 默认异步执行,先入库,后续异步执行,不影响主线程快速返回结果,毕竟失败了有重试,而且主线程的事务已经提交了,串行执行没啥意义。
|
||||
* 同步执行适合mq消费场景等对耗时不关心,但是希望链路追踪不被异步影响的场景。
|
||||
*
|
||||
* @return 是否异步执行
|
||||
*/
|
||||
boolean async() default true;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.abin.mallchat.transaction.annotation;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public interface SecureInvokeConfigurer {
|
||||
|
||||
/**
|
||||
* 返回一个线程池
|
||||
*/
|
||||
@Nullable
|
||||
default Executor getSecureInvokeExecutor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.abin.mallchat.transaction.aspect;
|
||||
|
||||
import com.abin.mallchat.transaction.annotation.SecureInvoke;
|
||||
import com.abin.mallchat.transaction.domain.dto.SecureInvokeDTO;
|
||||
import com.abin.mallchat.transaction.domain.entity.SecureInvokeRecord;
|
||||
import com.abin.mallchat.transaction.service.SecureInvokeService;
|
||||
import com.abin.mallchat.utils.JsonUtils;
|
||||
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.reflect.MethodSignature;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Description: 安全执行切面
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-04-20
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE + 1)//确保最先执行
|
||||
@Component
|
||||
public class SecureInvokeAspect {
|
||||
@Autowired
|
||||
private SecureInvokeService secureInvokeService;
|
||||
|
||||
@Around("@annotation(secureInvoke)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, SecureInvoke secureInvoke) throws Throwable {
|
||||
boolean async = secureInvoke.async();
|
||||
boolean inTransaction = TransactionSynchronizationManager.isActualTransactionActive();
|
||||
//非事务状态,直接执行,不做任何保证。
|
||||
if (!inTransaction) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
|
||||
List<String> parameters = Stream.of(method.getParameterTypes()).map(Class::getName).collect(Collectors.toList());
|
||||
SecureInvokeDTO dto = SecureInvokeDTO.builder()
|
||||
.args(JsonUtils.toStr(joinPoint.getArgs()))
|
||||
.className(method.getDeclaringClass().getName())
|
||||
.methodName(method.getName())
|
||||
.parameterTypes(JsonUtils.toStr(parameters))
|
||||
.build();
|
||||
SecureInvokeRecord record = SecureInvokeRecord.builder()
|
||||
.secureInvokeDTO(dto)
|
||||
.maxRetryTimes(secureInvoke.maxRetryTimes())
|
||||
.build();
|
||||
secureInvokeService.invoke(record, async);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.abin.mallchat.transaction.config;
|
||||
|
||||
import com.abin.mallchat.transaction.annotation.SecureInvokeConfigurer;
|
||||
import com.abin.mallchat.transaction.aspect.SecureInvokeAspect;
|
||||
import com.abin.mallchat.transaction.dao.SecureInvokeRecordDao;
|
||||
import com.abin.mallchat.transaction.mapper.SecureInvokeRecordMapper;
|
||||
import com.abin.mallchat.transaction.service.MQProducer;
|
||||
import com.abin.mallchat.transaction.service.SecureInvokeService;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-08-06
|
||||
*/
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
@MapperScan(basePackageClasses = SecureInvokeRecordMapper.class)
|
||||
@Import({SecureInvokeAspect.class, SecureInvokeRecordDao.class})
|
||||
public class TransactionAutoConfiguration {
|
||||
|
||||
@Nullable
|
||||
protected Executor executor;
|
||||
|
||||
/**
|
||||
* Collect any {@link AsyncConfigurer} beans through autowiring.
|
||||
*/
|
||||
@Autowired
|
||||
void setConfigurers(ObjectProvider<SecureInvokeConfigurer> configurers) {
|
||||
Supplier<SecureInvokeConfigurer> configurer = SingletonSupplier.of(() -> {
|
||||
List<SecureInvokeConfigurer> candidates = configurers.stream().collect(Collectors.toList());
|
||||
if (CollectionUtils.isEmpty(candidates)) {
|
||||
return null;
|
||||
}
|
||||
if (candidates.size() > 1) {
|
||||
throw new IllegalStateException("Only one SecureInvokeConfigurer may exist");
|
||||
}
|
||||
return candidates.get(0);
|
||||
});
|
||||
executor = Optional.ofNullable(configurer.get()).map(SecureInvokeConfigurer::getSecureInvokeExecutor).orElse(ForkJoinPool.commonPool());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecureInvokeService getSecureInvokeService(SecureInvokeRecordDao dao) {
|
||||
return new SecureInvokeService(dao, executor);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MQProducer getMQProducer() {
|
||||
return new MQProducer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.abin.mallchat.transaction.dao;
|
||||
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.abin.mallchat.transaction.domain.entity.SecureInvokeRecord;
|
||||
import com.abin.mallchat.transaction.mapper.SecureInvokeRecordMapper;
|
||||
import com.abin.mallchat.transaction.service.SecureInvokeService;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-08-06
|
||||
*/
|
||||
@Component
|
||||
public class SecureInvokeRecordDao extends ServiceImpl<SecureInvokeRecordMapper, SecureInvokeRecord> {
|
||||
|
||||
public List<SecureInvokeRecord> getWaitRetryRecords() {
|
||||
Date now = new Date();
|
||||
//查2分钟前的失败数据。避免刚入库的数据被查出来
|
||||
DateTime afterTime = DateUtil.offsetMinute(now, (int) SecureInvokeService.RETRY_INTERVAL_MINUTES);
|
||||
return lambdaQuery()
|
||||
.eq(SecureInvokeRecord::getStatus, SecureInvokeRecord.STATUS_WAIT)
|
||||
.lt(SecureInvokeRecord::getNextRetryTime, new Date())
|
||||
.lt(SecureInvokeRecord::getCreateTime, afterTime)
|
||||
.list();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.abin.mallchat.transaction.domain.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-08-06
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SecureInvokeDTO {
|
||||
private String className;
|
||||
private String methodName;
|
||||
private String parameterTypes;
|
||||
private String args;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.abin.mallchat.transaction.domain.entity;
|
||||
|
||||
import com.abin.mallchat.transaction.domain.dto.SecureInvokeDTO;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Description:
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-08-06
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName(value = "secure_invoke_record", autoResultMap = true)
|
||||
public class SecureInvokeRecord {
|
||||
public final static byte STATUS_WAIT = 1;
|
||||
public final static byte STATUS_FAIL = 2;
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
/**
|
||||
* 请求快照参数json
|
||||
*/
|
||||
@TableField(value = "secure_invoke_json", typeHandler = JacksonTypeHandler.class)
|
||||
private SecureInvokeDTO secureInvokeDTO;
|
||||
/**
|
||||
* 状态 1待执行 2已失败
|
||||
*/
|
||||
@TableField("status")
|
||||
@Builder.Default
|
||||
private byte status = SecureInvokeRecord.STATUS_WAIT;
|
||||
/**
|
||||
* 下一次重试的时间
|
||||
*/
|
||||
@TableField("next_retry_time")
|
||||
@Builder.Default
|
||||
private Date nextRetryTime = new Date();
|
||||
/**
|
||||
* 已经重试的次数
|
||||
*/
|
||||
@TableField("retry_times")
|
||||
@Builder.Default
|
||||
private Integer retryTimes = 0;
|
||||
@TableField("max_retry_times")
|
||||
private Integer maxRetryTimes;
|
||||
@TableField("fail_reason")
|
||||
private String failReason;
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.abin.mallchat.transaction.mapper;
|
||||
|
||||
import com.abin.mallchat.transaction.domain.entity.SecureInvokeRecord;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
public interface SecureInvokeRecordMapper extends BaseMapper<SecureInvokeRecord> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.abin.mallchat.transaction.service;
|
||||
|
||||
import com.abin.mallchat.transaction.annotation.SecureInvoke;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
/**
|
||||
* Description: 发送mq工具类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-08-12
|
||||
*/
|
||||
public class MQProducer {
|
||||
|
||||
@Autowired
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
public void sendMsg(String topic, Object body) {
|
||||
Message<Object> build = MessageBuilder.withPayload(body).build();
|
||||
rocketMQTemplate.send(topic, build);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送可靠消息,在事务提交后保证发送成功
|
||||
*
|
||||
* @param topic
|
||||
* @param body
|
||||
*/
|
||||
@SecureInvoke
|
||||
public void sendSecureMsg(String topic, Object body, Object key) {
|
||||
Message<Object> build = MessageBuilder
|
||||
.withPayload(body)
|
||||
.setHeader("KEYS", key)
|
||||
.build();
|
||||
rocketMQTemplate.send(topic, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.abin.mallchat.transaction.service;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.abin.mallchat.transaction.dao.SecureInvokeRecordDao;
|
||||
import com.abin.mallchat.transaction.domain.dto.SecureInvokeDTO;
|
||||
import com.abin.mallchat.transaction.domain.entity.SecureInvokeRecord;
|
||||
import com.abin.mallchat.utils.JsonUtils;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: 安全执行处理器
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-08-20
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class SecureInvokeService {
|
||||
|
||||
public static final double RETRY_INTERVAL_MINUTES = 2D;
|
||||
|
||||
private final SecureInvokeRecordDao secureInvokeRecordDao;
|
||||
|
||||
private final Executor executor;
|
||||
|
||||
@Scheduled(cron = "*/5 * * * * ?")
|
||||
public void retry() {
|
||||
List<SecureInvokeRecord> secureInvokeRecords = secureInvokeRecordDao.getWaitRetryRecords();
|
||||
for (SecureInvokeRecord secureInvokeRecord : secureInvokeRecords) {
|
||||
doAsyncInvoke(secureInvokeRecord);
|
||||
}
|
||||
}
|
||||
|
||||
public void save(SecureInvokeRecord record) {
|
||||
secureInvokeRecordDao.save(record);
|
||||
}
|
||||
|
||||
private void retryRecord(SecureInvokeRecord record, String errorMsg) {
|
||||
Integer retryTimes = record.getRetryTimes() + 1;
|
||||
SecureInvokeRecord update = new SecureInvokeRecord();
|
||||
update.setId(record.getId());
|
||||
update.setFailReason(errorMsg);
|
||||
update.setNextRetryTime(getNextRetryTime(retryTimes));
|
||||
if (retryTimes > record.getMaxRetryTimes()) {
|
||||
update.setStatus(SecureInvokeRecord.STATUS_FAIL);
|
||||
} else {
|
||||
update.setRetryTimes(retryTimes);
|
||||
}
|
||||
secureInvokeRecordDao.updateById(update);
|
||||
}
|
||||
|
||||
private Date getNextRetryTime(Integer retryTimes) {//或者可以采用退避算法
|
||||
double waitMinutes = Math.pow(RETRY_INTERVAL_MINUTES, retryTimes);//重试时间指数上升 2m 4m 8m 16m
|
||||
return DateUtil.offsetMinute(new Date(), (int) waitMinutes);
|
||||
}
|
||||
|
||||
private void removeRecord(Long id) {
|
||||
secureInvokeRecordDao.removeById(id);
|
||||
}
|
||||
|
||||
public void invoke(SecureInvokeRecord record, boolean async) {
|
||||
boolean inTransaction = TransactionSynchronizationManager.isActualTransactionActive();
|
||||
//非事务状态,直接执行,不做任何保证。
|
||||
if (!inTransaction) {
|
||||
return;
|
||||
}
|
||||
//保存执行数据
|
||||
save(record);
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
//事务后执行
|
||||
if (async) {
|
||||
doAsyncInvoke(record);
|
||||
} else {
|
||||
doInvoke(record);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void doAsyncInvoke(SecureInvokeRecord record) {
|
||||
executor.execute(() -> {
|
||||
System.out.println(Thread.currentThread().getName());
|
||||
doInvoke(record);
|
||||
});
|
||||
}
|
||||
|
||||
public void doInvoke(SecureInvokeRecord record) {
|
||||
SecureInvokeDTO secureInvokeDTO = record.getSecureInvokeDTO();
|
||||
try {
|
||||
Class<?> beanClass = Class.forName(secureInvokeDTO.getClassName());
|
||||
Object bean = SpringUtil.getBean(beanClass);
|
||||
List<String> parameterStrings = JsonUtils.toList(secureInvokeDTO.getParameterTypes(), String.class);
|
||||
List<Class<?>> parameterClasses = getParameters(parameterStrings);
|
||||
Method method = ReflectUtil.getMethod(beanClass, secureInvokeDTO.getMethodName(), parameterClasses.toArray(new Class[]{}));
|
||||
Object[] args = getArgs(secureInvokeDTO, parameterClasses);
|
||||
//执行方法
|
||||
method.invoke(bean, args);
|
||||
//执行成功更新状态
|
||||
removeRecord(record.getId());
|
||||
} catch (Throwable e) {
|
||||
log.error("SecureInvokeService invoke fail", e);
|
||||
//执行失败,等待下次执行
|
||||
retryRecord(record, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Object[] getArgs(SecureInvokeDTO secureInvokeDTO, List<Class<?>> parameterClasses) {
|
||||
JsonNode jsonNode = JsonUtils.toJsonNode(secureInvokeDTO.getArgs());
|
||||
Object[] args = new Object[jsonNode.size()];
|
||||
for (int i = 0; i < jsonNode.size(); i++) {
|
||||
Class<?> aClass = parameterClasses.get(i);
|
||||
args[i] = JsonUtils.nodeToValue(jsonNode.get(i), aClass);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<Class<?>> getParameters(List<String> parameterStrings) {
|
||||
return parameterStrings.stream().map(name -> {
|
||||
try {
|
||||
return Class.forName(name);
|
||||
} catch (ClassNotFoundException e) {
|
||||
log.error("SecureInvokeService class not fund", e);
|
||||
}
|
||||
return null;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user