序
今天没有空详细写,就说说现在的需求场景,就是希望插入业务操作日志,springCloud环境。然后,基于我实现的结果,还是说一句,设计没有公式,就是基础、特性的串联。
一、设计思路
经过分析,以及找其他组同事取经,大致的设计思路就分未3类,如下:
1、基于切面AOP
2、基于事件,EventBus或SpringBus
3、基于消息中间件
二、取舍历程
1.基于切面(舍)
因为是springCloud分布式场景,如果用切面,那么就有2个方案:
A、每个服务自己切面处理
这样处理我想大家都不愿意,这就意味着每个服务都得有一套AOP切面的代码,太繁琐,而且对于业务后期可能需要插入复合型业务操作日志。那么传参什么的都有可能需要变更,要动业务接口的传参,改动太大,风险太高,而且繁琐,舍。
B、在网关做切面
网关做的事情就不再纯粹了,失去了解耦性,对于要插入复合型业务操作日志,可能也会面临要修改业务接口的传参。。。舍
2.基于事件,EventBus或SpringBus(舍)
首先说场景,这2个对于单服务springBoot,那肯定是特别香。但是这里也要比较下他们的差异,直接丢结论:
看官方文档大致就可以知道,这2种方式都不适合SpringCloud分布式场景。
3.基于消息中间件
这里因为业务操作日志这样一个轻量型的业务场景,采用的redis的信息发布订阅。redis的发布订阅之前也有实现,这里我就直接分享核心代码,带点业务设计的。
RedisSubListenerConfig
import cn.hutool.core.util.ArrayUtil;
import com.fillersmart.fsihouse.commonservice.component.RedisReceiver;
import com.fillersmart.fsihouse.data.constant.ConstantsEnum.MsgType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;
/**
* redis订阅监听配置
*
* @author zhengwen
**/
@Component
public class RedisSubListenerConfig {
@Value("${redis.msg.topics}")
private String topics;
/**
* 初始化监听器
*
* @param connectionFactory 连接工厂
* @param listenerAdapter 监听适配器
* @return redis监听容器
*/
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
List<PatternTopic> topicList = new ArrayList<>();
// new PatternTopic("这里是监听的通道的名字") 通道要和发布者发布消息的通道一致
if (StringUtils.isNotBlank(topics)) {
String[] topisArr = topics.split(",");
if (ArrayUtil.isNotEmpty(topisArr)) {
Arrays.stream(topisArr).forEach(c -> {
PatternTopic topic = new PatternTopic(c);
topicList.add(topic);
});
}
}
//枚举信息通道
Arrays.stream(MsgType.values()).forEach(m -> {
PatternTopic topic = new PatternTopic(m.getType());
topicList.add(topic);
});
container.addMessageListener(listenerAdapter, topicList);
return container;
}
/**
* 绑定消息监听者和接收监听的方法
*
* @param redisReceiver redis接收人
* @return 信息监听适配器
*/
@Bean
MessageListenerAdapter listenerAdapter(RedisReceiver redisReceiver) {
// redisReceiver 消息接收者
// receiveMessage 消息接收后的方法
return new MessageListenerAdapter(redisReceiver, "receiveMessage");
}
@Bean
StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
/**
* 注册订阅者
*
* @param latch CountDownLatch
* @return RedisReceiver
*/
@Bean
RedisReceiver receiver(CountDownLatch latch) {
return new RedisReceiver(latch);
}
/**
* 计数器,用来控制线程
*
* @return CountDownLatch
*/
@Bean
CountDownLatch latch() {
//指定了计数的次数 1
return new CountDownLatch(1);
}
}
RedisReceiver
import cn.hutool.json.JSONUtil;
import com.fillersmart.fsihouse.commonservice.service.CommonService;
import com.fillersmart.fsihouse.data.core.Result;
import com.fillersmart.fsihouse.data.vo.msgpush.RedisMsgVo;
import java.util.concurrent.CountDownLatch;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/***
* 消息接收者(订阅者) 需要注入到springboot中
* @author zhengwen
*/
@Slf4j
@Component
public class RedisReceiver {
private CountDownLatch latch;
@Resource
private CommonService commonService;
@Autowired
public RedisReceiver(CountDownLatch latch) {
this.latch = latch;
}
/**
* 收到通道的消息之后执行的方法
*
* @param message
*/
public void receiveMessage(String message) {
//这里是收到通道的消息之后执行的方法
log.info("common通用服务收到redis信息:" + message);
if (JSONUtil.isTypeJSON(message)) {
RedisMsgVo redisMsgVo = JSONUtil.toBean(message,RedisMsgVo.class);
Result<?> res = commonService.dealRedisMsg(redisMsgVo);
log.info("--redis消息处理结果:{}",JSONUtil.toJsonStr(res));
}
latch.countDown();
}
}
一下是业务相关
dealRedisMsg方法
@Override
public Result<?> dealRedisMsg(RedisMsgVo redisMsgVo) {
log.info("--通用服务common处理redis消息--");
String queueName = redisMsgVo.getQueueName();
if (StringUtils.isBlank(queueName)) {
return ResultGenerator.genFailResult("redis信息队列名称为空");
}
MsgType msgType = MsgType.getEnum(queueName);
if (msgType == null) {
return ResultGenerator.genFailResult("未知的redis信息队列名称");
}
String msgContent = redisMsgVo.getContent();
switch (msgType) {
case PAYED_MSG:
//支付后信息
break;
case BUSINESS_OPERATE_LOG_MSG:
//业务操作记录
if (JSONUtil.isTypeJSON(msgContent)) {
BusinessOperateLogVo businessOperateLogVo = JSONUtil.toBean(msgContent,
BusinessOperateLogVo.class);
if (businessOperateLogVo == null) {
log.error("业务操作记录{}转换为空", msgContent);
} else {
businessOperateLogService.addBusLog(businessOperateLogVo);
}
}
break;
default:
break;
}
return ResultGenerator.genSuccessResult();
}
addBusLog方法
@Override
@Async
public Result<?> addBusLog(BusinessOperateLogVo businessOperateLogVo) {
//参数校验
Assert.notNull(businessOperateLogVo.getPlatform(), ResponseCodeI18n.PLATFORM_NULL.getMsg());
Assert.notNull(businessOperateLogVo.getOperateUserId(),
ResponseCodeI18n.OPERATE_USER_NULL.getMsg());
Assert.notNull(businessOperateLogVo.getBusinessId(),
ResponseCodeI18n.BUSINESS_ID_NULL.getMsg());
Assert.notNull(businessOperateLogVo.getCompanyId(), ResponseCodeI18n.PROJECT_ID_NULL.getMsg());
//初始操作人信息
initOperateUserName(businessOperateLogVo);
//初始业务操作信息
BusinessOperateLog operateLog = initBusinseeOperateLog(businessOperateLogVo);
businessOperateLogMapper.insert(operateLog);
return ResultGenerator.genSuccessResult();
}
调用方法
ThreadUtil.execAsync(() -> {
//异步转换并发送redis信息
BusinessOperateVo businessOperateVo = new BusinessOperateVo(operateUserId.longValue(),DevicePlatformType.OPERATION_PC);
//合同操作记录
BusinessDataVo businessDataVo = new BusinessDataVo(JSONObject.parseObject(JSONObject.toJSONString(finalUserSubscribe)),
BusinessDataType.SUBSCRIBE_DATA,BusinessOperateType.BACK_APPLY,BusinessType.SUBSCRIBE);
businessOperateVo.getBusinessDataList().add(businessDataVo);
//退租单的日志
businessDataVo = new BusinessDataVo(JSONObject.parseObject(JSONObject.toJSONString(backApply)),
BusinessDataType.BACK_APPLY_DATA, BusinessOperateType.BACK_APPLY,BusinessType.BACK_APPLY);
businessOperateVo.getBusinessDataList().add(businessDataVo);
businessOperateLogRpcService.convertLogSendToRedis(businessOperateVo);
});
补充表结构
CREATE TABLE `business_operate_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`company_id` int(11) DEFAULT NULL COMMENT '项目id',
`operate_type` int(2) DEFAULT NULL COMMENT '操作类型',
`operate_name` varchar(200) DEFAULT NULL COMMENT '操作名称',
`operate_time` datetime DEFAULT NULL COMMENT '操作时间',
`business_id` bigint(20) DEFAULT NULL COMMENT '业务数据主键id',
`business_type` int(2) DEFAULT NULL COMMENT '业务类型',
`operate_user_id` bigint(20) DEFAULT NULL COMMENT '操作用户id',
`operate_user_name` varchar(200) DEFAULT NULL COMMENT '操作人名称(冗余)',
`platform` int(1) DEFAULT NULL COMMENT '操作平台来源',
`memo` varchar(500) DEFAULT NULL COMMENT '备注',
`create_by` int(11) DEFAULT NULL COMMENT '创建人id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`is_deleted` int(1) DEFAULT NULL COMMENT '是否删除,0否,1是',
PRIMARY KEY (`id`),
KEY `business_operate_log_company_id_IDX` (`company_id`) USING BTREE,
KEY `business_operate_log_business_type_IDX` (`business_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=246 DEFAULT CHARSET=utf8mb4 COMMENT='业务日志记录';
补充日志存储截图
总结
1、ThreadUtil真香(HuTool的工具包),异步不影响接口性能
2、业务触发点只需要关注业务操作日志的关键传参
3、场景贯穿分布式场景
好了,时间比较紧,就写到这里,希望能帮到大家,有疑问欢迎留言。