[后端] 对全局高并发写流量字段通用写的技术方案

本文介绍了如何使用Redis缓存流量数据,通过流量类型和表名区分,以及结合Xxl-Job进行定时消费,详细描述了数据缓存的两层架构和消费过程,包括从Redis获取数据并更新数据库的操作。
摘要由CSDN通过智能技术生成

需求:

现在用户浏览不同的页面 , 需要对不同表产生流量数据增加. 这些表暂时称之为流量表

流量表的定义:

        1.流量表一般拥有右边这些字段: 点击量字段,收藏量字段,浏览量字段....

        2.流量表这些流量字段短时间内需要一直更改,频繁操作数据库

解决:

--解决此需求所用到的组件:

Redis + Xxl-Job + Mybatis-plus + SpringBoot ...    等开发常用组件;

--大致思路:

1.利用Redis缓存流量数据  2.利用Xxl-Job定时消费Redis缓存的数据

--具体思路(缓存和消费):

1.如何缓存流量数据?

        缓存流量数据后,所缓存的数据必须能够解答下面这个问题:

                                                                "这是哪个表的哪条数据的哪个字段的值?"

        那么拆分这个问题,得到这些小需求:

                1.这个数据属于哪个表 ?

                2.主键id是什么?

                3.值是多少?(为负代表减少,为正代表+)

                3.是针对哪个字段(点击量字段,收藏量字段,浏览量字段....)? 

        再次获取小需求的本质:

                1.需要区分流量类型

                2.需要区分表

                3.需要区分每条数据

                4.需要记录值

       因此使用2层的架构来缓存数据:

                第一级介绍: 

                         名称 :流量类型注册层

                         作用:记录流量类型,记录表

                         采取数据类型: Set

                         数据类型结构: key + members

                         数据类型特点: member不允许重复

                         key: 流量类型,例如是曝光量那么就叫:view_count

                         member:Service相对路径名称 ,例如是文章表 = com.cn.service.impl.Article

                 第一级抽象场景: 

                                当 key= view_count 

                                当member1= com.cn.service.impl.Article , 

                        代表曝光量类型下,有一个文章service背后代表的表 , 产生了曝光量数据

                第一级实际场景截图:

        

                 

                第二级介绍:

                         名称 :service流量数据缓存

                         作用: 记录service ,记录主键id,记录流量值

                         采取数据类型: Map

                         数据类型结构: member+ keys+ values

                         数据类型特点:

                                1个member有多个keys  key不允许重复 ,每个key对应一个value 

                         member: Service相对路径名称 ,例如是文章表 =com.cn.service.impl.Article

                         key = 主键id 例如:123

                         value:流量值,例如10

                第二级抽象场景:

                                当member=com.cn.service.impl.Article

                                ,当key=123

                                ,当value=10,

                        代表文章表主键为123的数据的某个流量字段的值需要+10

                第二级实际场景截图:

                第一级和第二级合并时产生的场景:

                        第一级:  

                                当 key= view_count 

                                当 member1= com.cn.service.impl.Article

                        第二级:  

                                当member=com.cn.service.impl.Article ,

                                当key=123,

                                当value=10

                        代表文章表 主键为123的数据 的曝光量需要+10

2.如何消费流量数据?

       定义一些东西:

                由于1级架构缓存的数据不会很多,流量类型基本就那几种,因此可以定义一个枚举来定义所有流量类型,

       

        从redis取出数据

                遍历流量类型枚举,比如遍历到CLICK这个成员, 那么就根据它获取它下面所有注册的service

                再根据所有service就能从二级的map获取这个service下面的所有流量数据,此时就成功从redis拿出所有的流量数据了

                代码实现如下:

 /**
     * 定时读取Redis流量数据写入MySQL
     */
    public void timingSaveFlowData() throws Exception {

        for (FlowType t : FlowType.getAll()) {

            //获取当前流量类型下 所有待处理的数据 key=service名称 value=数据
            Map<String, List<IdAndNumberDTO>> d = getPendingData( t );

            //写入数据到DB
            saveFlowDataToDb( d,t );

        }
    }

        


    /**
     * 获取所有待处理的数据
     * @return 待处理的数据 key=service名称 value=待处理的数据
     */
    private Map<String, List<IdAndNumberDTO>> getPendingData(FlowType t) {

        //获取当前流量下 所有已注册的 Service
        Set<Object> ss = redisTemplate.opsForSet().members( t.getValue() );

        //若当前流量下 没有被注册任何 Service 那么立即结束
        if(CollUtil.isEmpty( ss )){ return MapUtils.newZeroHashMap(); }

        //否则开始构建数据...
        Map<String,List<IdAndNumberDTO>> r = new HashMap<>( ss.size() );
        for (Object os : ss) {
            String s = (String) os;

            //获取当前Service下 所有待更新的数据
            Map<Object, Object> d = redisTemplate.opsForHash().entries( s );

            //没有数据 跳过这个Service
            if( CollUtil.isEmpty( d )){ continue; }

            //有数据   那么就把Map<Object, Object> 格式的数据 转为 List<IdAndNumberDTO> 格式的
            List<IdAndNumberDTO> ins = IdAndNumberDTO.of( d );

            //将构建好的数据 写入结果集
            r.put( s, ins );
        }

        //返回 结果集
        return r;
    }

        开始操作MySQL写入流量数据

                直接贴代码吧,主要关键字就是  类对象,反射,bean对象,mybatis-plus方法


    /**
     * 保存流量数据到数据库
     * @param d 流量数据
     * @param t 流量类型
     */
    private void saveFlowDataToDb(Map<String, List<IdAndNumberDTO>> d, FlowType t) throws Exception {

        //----------------------------------------------------校验-------------------------------------------------------

        //空数据 立即结束
        if(CollUtil.isEmpty( d )){ return; }

        //--------------------------------------------------核心逻辑-----------------------------------------------------

        //开始处理每个service的数据
        for (Map.Entry<String, List<IdAndNumberDTO>> e : d.entrySet()) {

            //<---------------------------------------------service处理------------------------------------------------->

            //获取解密后的service名称
            String s = RedisTemplateHelper.decodeKey( e.getKey() );
            //纠正service名称
            s = correctService(s);
            Class<?> cls;
            try {
                //通过类全路径获取class对象
                cls = Class.forName( s );
            } catch (ClassNotFoundException ex) {
                log.error( "这个路径的类找不到: {}",s );
                throw new RuntimeException( ex );
            }
            //通过service名称获取bean
            Object bean = applicationContextHelper.getBeanByServicePath( s );

            //<-----------------------------------------------数据处理-------------------------------------------------->

            //获取待更新的数据
            Map<String, IdAndNumberDTO> inMap = ListUtils.toMap( e.getValue(), IdAndNumberDTO::getId );

            //获取所有数据的主键
            List<String> ids = new ArrayList<>( inMap.keySet() );

            //<-----------------------------------------------数据查询-------------------------------------------------->

            //反射 mybatis-plus的 query 方法 执行查询数据
            Method listByIds = cls.getMethod( MYBATIS_PLUS_LISTBYIDS_NAME, Collection.class );
            List<?> pos = (List<?>) listByIds.invoke( bean, ids );
            if(CollUtil.isEmpty( pos )){ continue; }

            //<-----------------------------------------------数据更新-------------------------------------------------->

            //更新查询出来的po对象
            List<FlowBean> nPos = new ArrayList<>();
            for (Object p : pos) {
                //将p转为fp 方便获取成员数据   (注意此处是浅拷贝,如果改成深拷贝可能会让mybatisPlus更新时触发对象类型异常,这个没测试过)
                FlowBean fp = (FlowBean) p;

                //获取主键
                String id = fp.getId();
                //通过主键 获取待更新的值
                Integer nc = Math.toIntExact( inMap.get( id ).getNumber() );

                //原值 + 待更新的值 = 最终值
                if(FlowType.CLICK.equals( t )){
                    fp.setClickCount( fp.getClickCount() + nc );
                }
                else if(FlowType.VIEW.equals( t )){
                    fp.setViewCount( fp.getViewCount() + nc );
                }
                //将这个更新后的fp对象添加到nPos
                nPos.add( fp );
            }

            //<----------------------------------------------数据保存------------------------------------------------->

            //反射 mybatis-plus的 update方法 执行更新数据
            if(CollUtil.isEmpty( nPos )){ continue; }
            Method updateBatchById = cls.getMethod( MYBATIS_PLUS_UPDATEBATCHBYID_NAME, Collection.class );
            updateBatchById.invoke( bean, nPos );

        }

        //--------------------------------------------------其他处理----------------------------------------------------

        //删除已处理过的service
        redisTemplateHelper.deleteByKeys( new ArrayList<>( d.keySet() ) );

    }

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值