SpringBoot结合redis解决PV、UV亿级流量


文章目录

  • SpringBoot结合redis解决PV、UV亿级流量
  • 一 背景
  • 1. 初级开发视角
  • 2. 解决方案
  • 二 上代码
  • 1. 关系数据库表
  • 2. 切面设计
  • 3. 测试
  • 4. 数据同步
  • 三 交互
  • 1. 知识详情交互
  • 2. 转发知识交互
  • 四 总结


一 背景

1. 初级开发视角

文章浏览量统计,最傻的做法就是:用户每次浏览,前端会发送一个GET请求获取一篇文章详情时,会把这篇文章的浏览量+1,存进数据库里。

分析存在的问题:

  1. 在GET请求的业务逻辑里进行了数据的写操作!
  2. 高并发,数据库压力太大,文章浏览量+1会存在线程不安全问题,加锁会很慢。
  3. 同时,如果文章做了缓存和搜索引擎如ElasticSearch的存储,同步更新缓存和ElasticSearch更新同步更新太耗时,不更新就会导致数据不一致性。

2. 解决方案

HyperLogLog
HyperLogLog是Probabilistic data Structures的一种,这类数据结构的基本大的思路就是使用统计概率上的算法,牺牲数据的精准性来节省内存的占用空间及提升相关操作的性能。

设计思路
为保证真实的知识博文浏览量,根据用户访问的ip和文章id,进行唯一校验,即同一个用户多次访问同一篇文章,该文章访问量只增加1;
将用户的浏览量用opsForHyperLogLog().add(key,value)的存储在Redis中,在半夜浏览量低的时候,通过定时任务,将浏览量更新至数据库中。

核心点

日UV: key = 文章id, value = ip+当天时间 去重
	
	独立UV: key = 文章id, value = ip去重
	
	转发UV: key = 文章id-转发人id, value = ip去重
	
	
	在不同命名空间下:
	
	日登录UV: key = 文章id, value = 会员id+当天时间 去重
	
	登录独立UV: key = 文章id, value = 会员id 去重

单体架构图


redis uv redis uv pv_java

二 上代码

1. 关系数据库表

CREATE TABLE `kap_article` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `pv` bigint(20) DEFAULT '0' COMMENT '总浏览量',
  `uv` bigint(20) DEFAULT '0' COMMENT '总访客量',
  `zfl` bigint(20) DEFAULT '0' COMMENT '总转发量',
  `name` varchar(120) NOT NULL COMMENT '知识名称',
  `cover_img` varchar(500) NOT NULL COMMENT '知识封面图片',
  `show_status` tinyint(1) DEFAULT '1' COMMENT '显示状态(1=显示,0=不显示)',
  `content` text NOT NULL COMMENT '知识内容',
  `forward_ranking` tinyint(1) DEFAULT '1' COMMENT '转发排行(1=开启,0=关闭)',
  `ranking_show_num` bigint(20) DEFAULT '0' COMMENT '排行展示数(0=不限,n>0部分展示数从高到低)',
  `classify_id` bigint(20) NOT NULL COMMENT '分类id',
  `sort_by` int(11) DEFAULT '1' COMMENT '排序',
  `tenant_id` bigint(20) NOT NULL COMMENT '租户id',
  `is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除(1:是,0:否)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_user_name` varchar(50) DEFAULT NULL COMMENT '创建人',
  `update_user_name` varchar(50) DEFAULT NULL COMMENT '更新人',
  PRIMARY KEY (`id`),
  UNIQUE KEY `index_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='知识表';
注:可以插入了一条数据,并设计访问量已经为10了,便于测试。

2. 切面设计

1. 自定义一个注解

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PageView {

    /**
     * 缓存命名空间前缀
     */
    String prefix() default "";

}
  1. 切面处理
/**
 * @author yanghouqing
 * @date 2022-05-09
 */
@Aspect
@Configuration
@Slf4j
public class PageViewAspect {

    /**
     * 缓存命名空间
     */
    final String MAYBELIKE_NAMESPACE = "vee_saas_mall:statistics:";
    final String PV = "pv:";
    final String UV = "uv:";
    final String UV_DAY = "uv:day:";
    
    @Autowired
    private RedisUtils redisUtils;

    /**
     * 获取当前的ServletRequest
     * @return
     */
    protected HttpServletRequest servletRequest() {
        return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
    }

    /**
     * 切入点
     */
    @Pointcut("@annotation(cn.weiyisoft.platform.annotation.PageView)")
    public void pageViewAspect() {}

    /**
     * 切入处理
     * @param joinPoint
     * @return
     */
    @AfterReturning(value = "pageViewAspect() && @annotation(pageView)", returning = "result")
    public void around(JoinPoint joinPoint, PageView pageView, Result result) {
        log.info("@PageView请求返回内容:RESPONSE : {}", Objects.nonNull(result) ? result.toString() : "");
        if (200 != result.getCode()) {
            return;
        }
        // 先判断 pageView 是否为空, 为空则获取类上注解
        if (pageView == null) {
            Class<?> aClass = joinPoint.getTarget().getClass();
            pageView = AnnotationUtils.findAnnotation(aClass, PageView.class);
        }
        Object[] object = joinPoint.getArgs();
        Object id = object[0];
        Object transmitCode = object[1];
        log.info("id:{}", id);
        log.info("transmitCode:{}", transmitCode);
        this.cache(pageView,id);
        // 判断是否携带转发code
        if (ObjectUtils.isNotEmpty(transmitCode)) {
            this.transmitCache(pageView,id,transmitCode);
        }
    }


    /**
     * 缓存 主维度
     * @param pageView
     * @param id
     */
    private void cache(PageView pageView,Object id){
        // 缓存 PV
        redisUtils.incr(MAYBELIKE_NAMESPACE + pageView.prefix() + PV + id,1);
        // 缓存 UV
        redisUtils.add(MAYBELIKE_NAMESPACE + pageView.prefix() + UV + id, IpUtils.getIpAddr(servletRequest()));
        // 缓存 UV 日访客
        redisUtils.add(MAYBELIKE_NAMESPACE + pageView.prefix() + UV_DAY + id, IpUtils.getIpAddr(servletRequest())
                + "-" + DateUtil.format(new Date(), "yyyyMMdd"));
    }

    /**
     * 缓存 转发人维度
     * @param pageView
     * @param id
     * @param transmitCode
     */
    private void transmitCache(PageView pageView,Object id,Object transmitCode){
        // 缓存转发 PV
        redisUtils.incr(MAYBELIKE_NAMESPACE + pageView.prefix() + PV + id +"-"+ ShareCodeUtil.codeToId(transmitCode.toString()),1);
        // 缓存转发 UV
        redisUtils.add(MAYBELIKE_NAMESPACE + pageView.prefix() + UV + id +"-"+ ShareCodeUtil.codeToId(transmitCode.toString()), IpUtils.getIpAddr(servletRequest()));
        // 缓存转发 UV 日访客
        redisUtils.add(MAYBELIKE_NAMESPACE + pageView.prefix() + UV_DAY + id +"-"+ ShareCodeUtil.codeToId(transmitCode.toString()), IpUtils.getIpAddr(servletRequest())
                + "-" + DateUtil.format(new Date(), "yyyyMMdd"));
    }

}
  1. 工具类 RedisUtils
/**
 * @author yanghouqing
 * @date 2022-05-09
 */
@Component
public class RedisUtils {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取缓存
     * @param key
     */
    public Object get(String key) {
       return redisTemplate.opsForValue().get(key);
    }

    /**
     * 批量获取缓存
     * @param keys
     */
    public List<Object> mGet(List<String> keys) {
         return redisTemplate.opsForValue().multiGet(keys);
    }

    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    public void del(String... key) {
        redisTemplate.delete(key[0]);
    }

    /**
     * HyperLogLog计数
     * @param key
     * @param value
     */
    public Long add(String key, Object... value) {
        return redisTemplate.opsForHyperLogLog().add(key,value);
    }

    /**
     * HyperLogLog获取总数
     * @param key
     */
    public Long count(String key) {
        return redisTemplate.opsForHyperLogLog().size(key);
    }

    /**
     * HyperLogLog获取总数
     * @param key
     */
    public Long count(String... key) {
        return redisTemplate.opsForHyperLogLog().size(key);
    }

    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta){
        if(delta<0){
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta){
        if(delta<0){
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
}
  1. 工具类 IpUtils
public class IpUtils {
    //获取客户端IP地址
    public static String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknow".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
            if (ip.equals("127.0.0.1")) {
                //根据网卡取本机配置的IP
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                assert inet != null;
                ip = inet.getHostAddress();
            }
        }
        // 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ip != null && ip.length() > 15) {
            if (ip.indexOf(",") > 0) {
                ip = ip.substring(0, ip.indexOf(","));
            }
        }
        return ip;
    }
}
  1. 服务层列表高速查询逻辑 PageServices.java
@Autowired
private RedisUtils redisUtils;

    @Transactional(readOnly = true)
    public List<KapArticleDto> list(KapArticleQueryDto kapArticleQueryDto, PageParameter pageParameter) {
        PageHelper.startPage(pageParameter.getPage(), pageParameter.getSize()).setOrderBy(ParameterUtil.getSort(pageParameter));
        List<KapArticleDto> kapArticleDtos = this.kapArticleMapper.list(kapArticleQueryDto);
        List<String> idsKeysPV = kapArticleDtos.stream().map(a -> MAYBELIKE_NAMESPACE_PV + a.getId()).collect(Collectors.toList());
        List<Integer> pvInts = (List<Integer>)(List) redisUtils.mGet(idsKeysPV);
        List<Long> objectsPV = pvInts.stream().map(a -> a == null ? 0L : a).collect(Collectors.toList());

        AtomicInteger index = new AtomicInteger(0);
        kapArticleDtos = kapArticleDtos.stream().map(a -> {
            a.setPv(objectsPV.get(index.getAndIncrement()));
            try {
                a.setUv(redisUtils.count(MAYBELIKE_NAMESPACE_UV+a.getId()));
            } catch (Exception e) {
                log.error("批量获取文章UV失败跳过,文章:{},异常:{}",a,e);
            }
            return a;
        }).collect(Collectors.toList());
        return kapArticleDtos;
    }
  1. 测试接口 PageController.java
@RestController
@Slf4j
public class PageController {
    @Autowired
    private KapArticleService kapArticleService;
    
    @Autowired
    private RedisUtils redisUtils;
    
    @GetMapping(value = "/list")
    @ApiOperation("查询知识")
    public Result<PageInfo<KapArticleDto>> areaList(
            @Validated PageParameter pageParameter,
            @Validated KapArticleQueryDto kapArticleQueryDto
    ) {
        PageInfo<KapArticleDto> areaPageInfo = new PageInfo<>(this.kapArticleService.list(kapArticleQueryDto, pageParameter));
        return R.success(areaPageInfo);
    }

    @PageView(prefix = "kap:")
    @GetMapping(value = "/info")
    @ApiOperation("知识详情信息")
    public Result<KapArticleDto> areaInfo(
            @ApiParam(name="id", value = "id", required = true, example = "1") @NotNull(message = "id不允许为空") @RequestParam Long id,
            @ApiParam(name="code", value = "转发code",  example = "1") @RequestParam(required = false) String code
    ) {
        KapArticle kapArticle = this.kapArticleService.findById(id);
        if (null == kapArticle) {
            return R.fail("文章不存在");
        }
        KapArticleDto kapArticleDto = KapArticleDto.convertFrom(kapArticle);
        kapArticleDto.setPv((Long) redisUtils.get("vee_saas_mall:statistics:kap:pv:"+id));
        kapArticleDto.setUv(redisUtils.count("vee_saas_mall:statistics:kap:uv:"+id));
        return R.success(kapArticleDto);
    }
}

3. 测试

redis入库数据截图:

redis uv redis uv pv_spring boot_02

接口数据返回截图:

redis uv redis uv pv_java_03

4. 数据同步

数据同步指的是将redis中最新pvuv同步至mysql,包扣统计报表同步至elasticsearch

数据明细统计

  1. es结构
@Data
@ESMetaData(indexName = "kap_article_detail",number_of_shards = 3,number_of_replicas = 0,printLog = true)
public class KapArticleDetail implements Serializable {

    /**
     * id
     */
    @ESID
    private String id;

    /**
     * 日期
     */
    @ESMapping(datatype = DataType.date_type)
    private Date time;

    /**
     * 转发量
     */
    @ESMapping(datatype = DataType.long_type)
    private Long zfl;

    /**
     * 浏览量
     */
    @ESMapping(datatype = DataType.long_type)
    private Long pv;

    /**
     * 访客量
     */
    @ESMapping(datatype = DataType.long_type)
    private Long uv;

   	/**
     * 文章id
     */
    @ESMapping(datatype = DataType.long_type)
    private Long articleId;

}
  1. 定时任务
/**
 * 知识数据明细统计 每晚拉取redis中数据同步至es
 */
@Slf4j
@Component
@Transactional
public class KapArticleDetailTask {

    final String MAYBELIKE_NAMESPACE_PV = "vee_saas_mall:statistics:kap:pv:";
    final String MAYBELIKE_NAMESPACE_UV = "vee_saas_mall:statistics:kap:uv:day:";

    @Autowired
    private ElasticsearchTemplate<KapArticleDetail,String> elasticsearchTemplate;
    
    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private KapArticleService kapArticleService;

    private void auto() throws Exception {
        List<KapArticleDto> kapArticleDtos = kapArticleService.getAllAndZfl();
        log.info("知识数据明细统计,定时任务开始执行>>>>>>,文章条目数:{}", kapArticleDtos.size());
        Integer okCount = 0;
        for (KapArticleDto kapArticleDto : kapArticleDtos) {
            // 查询前天的的数据
            KapArticleDetail kapArticleDetailQT = elasticsearchTemplate.getById(
                    kapArticleDto.getId() +"-"+ DateUtil.format(getCreateTime(-2),"yyyyMMdd"),
                    KapArticleDetail.class);
            String idZT = kapArticleDto.getId() +"-"+ DateUtil.format(getCreateTime(-1),"yyyyMMdd");
            // 判断昨天的数据是否已经保存过
            boolean existsZT = elasticsearchTemplate.exists(idZT,KapArticleDetail.class);
            if (!existsZT) {
                KapArticleDetail kapArticleDetail = new KapArticleDetail();
                try {
                    kapArticleDetail.setId(idZT);
                    kapArticleDetail.setArticleId(kapArticleDto.getId());
                    Object pvs = redisUtils.get(MAYBELIKE_NAMESPACE_PV + kapArticleDto.getId());
                    Long uvs = redisUtils.count(MAYBELIKE_NAMESPACE_UV + kapArticleDto.getId());
                    long pv = Long.parseLong(pvs == null ? "0": pvs.toString());
                    long uv = uvs == null ? 0L: uvs;
                    if (null == kapArticleDetailQT) {
                        kapArticleDetail.setZfl(kapArticleDto.getZfl());
                        kapArticleDetail.setPv(pv);
                        kapArticleDetail.setUv(uv);
                    } else {
                        kapArticleDetail.setZfl(Math.max(kapArticleDto.getZfl() - kapArticleDetailQT.getZfl(), 0L));
                        kapArticleDetail.setPv(Math.max(pv - kapArticleDetailQT.getPv(), 0L));
                        kapArticleDetail.setUv(Math.max(pv - kapArticleDetailQT.getUv(), 0L));
                    }
                    kapArticleDetail.setTime(getCreateTime(-1));
                    boolean saveResult = elasticsearchTemplate.save(kapArticleDetail);
                    if (saveResult) okCount +=1;
                } catch (Exception e){
                    log.error("知识数据明细统计,插入es失败:{},异常信息:{}", new Gson().toJson(kapArticleDetail), e);
                }
            }
        }
        log.info("知识数据明细统计,定时任务执行完毕<<<<<<,文章总条目数:{},成功条目数:{}", kapArticleDtos.size(), okCount);
    }	

    /**
     * 获取当前时间前n天凌晨时间
     * @return
     */
    private Date getCreateTime(int n) {
        Calendar cal = Calendar.getInstance();
        cal.add(cal.DATE,n);
        cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), 0, 0, 0);
        Date beginOfDate = cal.getTime();
        return beginOfDate;
    }

}

转发排行榜统计

  1. es结构
@Data
@ESMetaData(indexName = "kap_article_transmit",number_of_shards = 3,number_of_replicas = 0,printLog = true)
public class KapArticleTransmit implements Serializable {

    /**
     * id
     */
    @ESID
    private String id;

    /**
     * 创建时间
     */
    @ESMapping(datatype = DataType.date_type)
    private Date createTime;

    /**
     * 更新时间
     */
    @ESMapping(datatype = DataType.date_type)
    private Date updateTime;

    /**
     * 合伙人
     */
    @ESMapping(datatype = DataType.keyword_type)
    private String nickName;

    /**
     * 手机号
     */
    @Sensitive(type = SensitiveTypeEnum.MOBILE_PHONE)
    @ESMapping(datatype = DataType.keyword_type)
    private String mobile;

    /**
     * 合伙人会员id
     */
    @ESMapping(datatype = DataType.long_type)
    private Long memberId;

    /**
     * 转发量
     */
    @ESMapping(datatype = DataType.long_type)
    private Long zfl;

    /**
     * 浏览量
     */
    @ESMapping(datatype = DataType.long_type)
    private Long pv;

    /**
     * 访客量
     */
    @ESMapping(datatype = DataType.long_type)
    private Long uv;

    /**
     * 文章id
     */
    @ESMapping(datatype = DataType.long_type)
    private Long articleId;

}
  1. 定时任务
/**
 * 知识转发排行统计 每晚拉取redis中数据同步至es
 */
@Slf4j
@Component
@Transactional
public class KapArticleTransmitTask {

    final String MAYBELIKE_NAMESPACE_PV = "vee_saas_mall:statistics:kap:pv:";
    final String MAYBELIKE_NAMESPACE_UV = "vee_saas_mall:statistics:kap:uv:";

    @Autowired
    private ElasticsearchTemplate<KapArticleTransmit,String> elasticsearchTemplate;

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private KapArticleService kapArticleService;

    private void auto() throws Exception {
        List<KapTransmitRankingDto> kapTransmitRankingDtos = kapArticleService.getAllAndZFPH();
        log.info("知识转发排行统计,定时任务开始执行>>>>>>,文章合伙人转发条目数:{}", kapTransmitRankingDtos.size());
        Integer saveCount = 0, updateCount = 0, expCount = 0;
        for (KapTransmitRankingDto kapTransmitRankingDto : kapTransmitRankingDtos) {
            String key = kapTransmitRankingDto.getArticleId() + "-" + kapTransmitRankingDto.getMemberId();

            KapArticleTransmit kapArticleTransmit = new KapArticleTransmit();
            kapArticleTransmit.setId(key);
            kapArticleTransmit.setNickName(kapTransmitRankingDto.getNickName());
            kapArticleTransmit.setMobile(kapTransmitRankingDto.getMobile());
            kapArticleTransmit.setMemberId(kapTransmitRankingDto.getMemberId());
            kapArticleTransmit.setZfl(kapTransmitRankingDto.getZfl());
            Object pvs = redisUtils.get(MAYBELIKE_NAMESPACE_PV + key);
            Long uvs = redisUtils.count(MAYBELIKE_NAMESPACE_UV + key);
            long pv = Long.parseLong(pvs == null ? "0": pvs.toString());
            long uv = uvs == null ? 0L: uvs;
            kapArticleTransmit.setPv(pv);
            kapArticleTransmit.setUv(uv);
            kapArticleTransmit.setArticleId(kapTransmitRankingDto.getArticleId());
            try {
                // 查询是否存在,存在就更新,不存在就新增。
                boolean exists = elasticsearchTemplate.exists(key, KapArticleTransmit.class);
                if (exists) {
                    kapArticleTransmit.setUpdateTime(new Date());
                    boolean updateResult = elasticsearchTemplate.update(kapArticleTransmit);
                    if (updateResult) updateCount += 1;
                } else {
                    kapArticleTransmit.setUpdateTime(new Date());
                    kapArticleTransmit.setCreateTime(new Date());
                    boolean saveResult = elasticsearchTemplate.save(kapArticleTransmit);
                    if (saveResult) saveCount += 1;
                }
            } catch (Exception e) {
                expCount += 1;
                log.error("知识数据明细统计,操作es失败:{},异常信息:{}", new Gson().toJson(kapArticleTransmit), e);
            }
        }
        log.info("知识转发排行统计,定时任务执行完毕<<<<<<," +
                "文章合伙人转发条目数:{},新增条目数:{},更新条目数:{},异常条目数:{}",
                kapTransmitRankingDtos.size(), saveCount, updateCount, expCount);
    }

}

三 交互

角色:合伙人,会员,游客

1. 知识详情交互

redis uv redis uv pv_redis uv_04

访客通过打开合伙人转发的链接进入知识页面,通过调用后端接口获取相关知识数据渲染,code入参字段重要,此时后端会根据转发code做响应的记录以及pv,uv等,如果点击预览提示访客注册,此时注册成功就会和合伙人做关联。

//获取知识
curl -X GET http://127.0.0.1:8903/api/kapArticle/info?id=5&code=XUHZD9

//Query参数
id = 知识id //必填参数

//head参数
code = 转发code //非必填参数,通过点击转发按钮生成的

//成功响应示例
{
    "code": 200,
    "msg": "SUCCESS", //返回文字描述
    "data": {
        "id": "4", //素材id
        "name": "库存品知识", //知识名称
        "coverImg": "a.jpg", //知识封面图片
        "showStatus": true, //显示状态
        "content": "2222222", //知识内容(富文本base64二进制数据)
        "forwardRanking": true, //转发排行
        "rankingShowNum": "0", //排行展示数(0=不限,n>0部分展示数从高到低)
        "classifyId": "4", //分类id
        "classifyName": "测试分类", //分类名称
        "sortBy": 1, //排序
        "tenantId": "10", //租户id
        "createTime": "2022-05-07 17:12:03", //创建时间
        "updateTime": "2022-05-10 10:51:33", //修改时间
        "pv": "10", //浏览量
        "uv": "1", //访客量
        "zfl": "1" //转发量
    } //返回数据
}

2. 转发知识交互

redis uv redis uv pv_redis uv_05

访客不可以点击转发按钮获得code,前端调用接口的时候记得判断是否有合伙人角色,后端也会进行二次验证。

//生成转发code接口
curl -X POST http://127.0.0.1:8903/api/kapTransmit/genTransmitCode?articleId=5&tenantId=1
//Query参数
articleId = 知识id
tenantId = 租户id

//成功响应示例
{
    "code": 200,
    "msg": "SUCCESS", //返回文字描述
    "data": {
        "code": "XUHZD9", //转发code
    } //返回数据
}

四 总结

注意:redis开启RDB持久化机制

如果大佬您满意不妨给小弟点个赞呗