今天没有空详细写,就说说现在的需求场景,就是希望插入业务操作日志,springCloud环境。然后,基于我实现的结果,还是说一句,设计没有公式,就是基础、特性的串联。


一、设计思路

经过分析,以及找其他组同事取经,大致的设计思路就分未3类,如下:
1、基于切面AOP
2、基于事件,EventBus或SpringBus
3、基于消息中间件

二、取舍历程

1.基于切面(舍)

因为是springCloud分布式场景,如果用切面,那么就有2个方案:
A、每个服务自己切面处理
这样处理我想大家都不愿意,这就意味着每个服务都得有一套AOP切面的代码,太繁琐,而且对于业务后期可能需要插入复合型业务操作日志。那么传参什么的都有可能需要变更,要动业务接口的传参,改动太大,风险太高,而且繁琐,舍。
B、在网关做切面
网关做的事情就不再纯粹了,失去了解耦性,对于要插入复合型业务操作日志,可能也会面临要修改业务接口的传参。。。舍

2.基于事件,EventBus或SpringBus(舍)

首先说场景,这2个对于单服务springBoot,那肯定是特别香。但是这里也要比较下他们的差异,直接丢结论:

redis中如何设置日志保留天数 redis的日志_基于redis消息的操作日志


看官方文档大致就可以知道,这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='业务日志记录';

补充日志存储截图

redis中如何设置日志保留天数 redis的日志_业务操作日志_02


总结

1、ThreadUtil真香(HuTool的工具包),异步不影响接口性能
2、业务触发点只需要关注业务操作日志的关键传参
3、场景贯穿分布式场景
好了,时间比较紧,就写到这里,希望能帮到大家,有疑问欢迎留言。