现在我们的项目首页已经供应了商品列表和商品详情的功能,那,现在是不是可以给商品加个浏览量的功能,毕竟这是常规操作。
ok,那开始实现功能之前,我们先来修正一下表构造和实体,向个中加入浏览量字段。
sql复制代码SQL:ALTER TABLE `sb_commodity` ADD COLUMN `views` INT DEFAULT 0 NULL COMMENT '浏览量' AFTER `content`;实体:private Integer views;
1、设计
先来定义一下,作甚浏览,即,用户点击了商品详情的时候视为一次浏览。
以是按照上面的说法,是不是每次用户点击了商品详情就给数据库字段 + 1 即可,就像下面这样:
sql复制代码UPDATE sb_commodity SET views = views + 1 WHERE id = ?
理论上,这个做法是可以的,但是这不是实现浏览量功能的全部,这仅仅只是末了一步罢了。为什么这么说,大家看我下面的思考:
用户每刷新一次商品详情,update 语句就实行一次吗?如果用户未登录,查看商品详情是否须要给浏览量 + 1?总结一下便是,是否每次刷新页面,浏览量就 + 1 和用户未登录,浏览量是否须要 + 1。当然,由这两个考虑我们又可以引申出很多个问题:
浏览量 + 1 每次都 update 数据库吗如果不是每次刷新一次浏览量就 + 1,那如何区分用户是否已经浏览了如果按用户 id 来区分是否浏览,那未登录的用户呢如果按照用户的 IP 来区分是否浏览,那该如何存呢用户 IP 是可以变革的,那如果同一个用户 IP 变革了,浏览量是否须要 + 1 呢那,在剖析出详细的实现方案之前,我们须要明白一件事情便是浏览量是一个大概的数字,也即可以不须要非常的精确,记住浏览量是一个大概的数字。
剖析到这里,我得出如下方案:
IP + 商品 id,作为唯一 key 存 Redis
key 带有过期韶光,如果在过期韶光内同 IP + 同商品 id 的 key 存在,则忽略
定时任务定时统计 IP + 商品 id 的 key 个数,存入数据库中
方案流程图:
末了咱们对着这个方案,来细品一下,Redis 用什么数据构造存储数据?
咱们的数据构造为,一个商品,对应很多个用户 IP第一种:List,通过前缀 + 商品 ID 为 key,用户 IP 为 value 向凑集中添加数据,当我们须要获取商品的浏览量时只须要获取凑集 size 即可。但你们又没想过一个问题,list 可是不去重的,也即用户可以不同的刷新页面导致 list 凑集中重复的 ip 不同的增加,以是这个构造弗成。
第二种:Set,这个就非常可以了,估计 set 的特性 value 不会存在重复的数据,也即用户不同的刷新页面同一个ieIP 只会记录一次。理论上,这个 set 就可以了,但是我们考虑一下,如果定时任务实行之前, 100 个商品,每个商品增长了 1 W 浏览量,这个内存占用,有没有考虑过?约 300 MB 的占用,这有点大啊,如果不止 100 个商品,那内存增长量更大。以是,还须要重新考虑一个即不重复又不怎么占用内存空间的数据构造。
第三种:HyperLogLog,是的,这个数据构培养是我们终极的办理方案,它的优点在于,元素不充分且在输入元素的数量或者体积非常非常大时,打算基数所需的空间总是固定的、并且是很小的值(约 12 k)。
2、实践终于到动手编码环节了,这次我们须要改动的点有如下三块:
查看商品时,增加浏览量记录功能获取商品列表时,回查商品浏览量功能定时同步 Redis 中的浏览量到 MySQL 中2.1 Redis 添加浏览量记录Redis 中我们的 key 构造为:前缀 + 商品 ID,value 为用户 IP。
在用户查看商品信息的时候,我们记录一下该商品的浏览量,也即把下面的方法,放入查看商品信息之中。
java复制代码// 添加商品浏览量addViews(commodity.getId());
实现:
java复制代码private void addViews(Long commodityId) { // 获取要求 request HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); // 获取 ip 和天生 key String ip = IpUtil.getClientIp(request); String key = SbUtil.getCommodityViewsKey(commodityId); redisTemplate.opsForHyperLogLog().add(key, ip);}
2.2 商品列表回查浏览量
再获取商品列表的时候,我们商品的浏览量不止是数据库的 view 字段了,还须要加上 Redis 中缓存的浏览量。以是在获取商品列表的时候,我们须要补充一下商品的浏览量,详细实现如下。
java复制代码// 回填一下 redis 中的商品浏览量fillViews(page.getRecords());
实现:
java复制代码private void fillViews(List<HomeCommodityVO> commodityList) { // 空则不处理 if (CollectionUtils.isEmpty(commodityList)) { return; } for (HomeCommodityVO commodityVO : commodityList) { // 存在 key 则补充浏览量 if (Boolean.TRUE.equals(redisTemplate.hasKey(SbUtil.getCommodityViewsKey(commodityVO.getId())))) { Long size = redisTemplate.opsForHyperLogLog().size(SbUtil.getCommodityViewsKey(commodityVO.getId())); commodityVO.setViews(commodityVO.getViews() + size.intValue()); } }}
2.3 定时同步浏览量到数据库
下面来到我们功能的最关键一步了,同步浏览量数据到数据库。
先说一下做这一步的目前是为了让数据能写到磁盘,由于 Redis 毕竟是内存数据库,断电即失落,而且 Redis 也不宜一贯占用过多的内存,以是这个数据落盘是一定要做的。
详细实现步骤:
获取商品浏览量的所有 key循环获取出 key 对应的 value 值删除所有 key遍历并分解 key 中的商品 id ,将商品的原有浏览量和获取 Redis 的浏览量相加,存入数据库,完成同步操作编码之前,在来看看该功能的详细流程图:
该功能的须要把稳的点我已经写在了流程图中,下面就开始编码吧!
1)schedule 编写
位置:cn.j3code.merchant.schedule
java复制代码@Slf4j@Component@AllArgsConstructorpublic class CommodityViewsSchedule { private final CommodityService commodityService; / 11,27,43,57 分钟行一次,商品的浏览量,在该韶光内该当增加不了多少 / @DistributedLock @Scheduled(cron = "45 11,27,43,57 ? ") public void fillCommodityViews() { StopWatch stopWatch = new StopWatch(); stopWatch.start("同步商品浏览量"); try { commodityService.fillCommodityViews(); } catch (Exception e) { log.error("同步商品浏览量出错:", e); } finally { stopWatch.stop(); log.info("同步商品浏览量实行韶光:{}", stopWatch.getTotalTimeSeconds()); } }}
2)service 编写
位置:
java复制代码public interface CommodityService extends IService<Commodity> { void fillCommodityViews();}@Slf4j@AllArgsConstructor@Servicepublic class CommodityServiceImpl extends ServiceImpl<CommodityMapper, Commodity> implements CommodityService { private final RedisTemplate<String, Object> redisTemplate; private final TransactionTemplate transactionTemplate; @Override public void fillCommodityViews() { // 获取所有浏览量 key Set<String> keys = redisTemplate.keys(SbUtil.getCommodityViewsKey(null) + ""); if (CollectionUtils.isEmpty(keys)) { return; } // key 和 浏览量 ,逐一对应 Map<String, Long> viewMap = keys.stream().collect(Collectors .toMap(key -> key, value -> redisTemplate.opsForHyperLogLog().size(value))); List<Commodity> updateCommodityList = viewMap.entrySet().stream().map(entry -> { Commodity commodity = new Commodity(); // 分解 key,取出 id commodity.setId(Long.parseLong(entry.getKey().substring(entry.getKey().lastIndexOf(":") + 1))); commodity.setViews(entry.getValue().intValue()); return commodity; }).collect(Collectors.toList()); // 获取数据库中的原始浏览量 Map<Long, Integer> commodityIdToViewMap = lambdaQuery().in(Commodity::getId, updateCommodityList.stream().map(Commodity::getId).collect(Collectors.toList())) .list().stream().collect(Collectors.toMap(Commodity::getId, Commodity::getViews)); // redis + MySQL 即是 总浏览量 updateCommodityList.forEach(item -> item.setViews(commodityIdToViewMap.get(item.getId()) + item.getViews())); / 在一个事务中实行修正 MySQL 和 删除 Redis 操作 / MyTransactionTemplate.execute(transactionTemplate, accept -> { // 分割凑集,每次批量更新 100 条数据 CollUtil.split(updateCommodityList, 100).forEach(list -> { // 批量修正 updateBatchById(updateCommodityList); }); // 移除 redis key redisTemplate.delete(keys); }, "同步浏览量失落败!
"); }}
我相信代码写的已经很详细了,但须要把稳一点便是,修正 MySQL 和删除 Redis 只管即便放在一个事务中实行,防止涌现实行失落败,丢失部分浏览量或者多打算浏览量的情形。
那,本片内容就到此结束了,咱们下回见。