需求:
现在用户浏览不同的页面 , 需要对不同表产生流量数据增加. 这些表暂时称之为流量表
流量表的定义:
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() ) ); }