• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

社交支付系统,商品浏览量设计和实践

武飞扬头像
J3code
帮助1

个人项目:社交支付项目(小老板)

文档系统:admire.j3code.cn/note

预览地址(未开发完):admire.j3code.cn/small-boss

  • 内网穿透部署,第一次访问比较慢

现在我们的项目首页已经提供了商品列表和商品详情的功能,那,现在是不是可以给商品加个浏览量的功能,毕竟这是常规操作。

ok,那开始实现功能之前,我们先来修改一下表结构和实体,向其中加入浏览量字段。

SQL:
ALTER TABLE `sb_commodity`   
	ADD COLUMN `views` INT DEFAULT 0 NULL COMMENT '浏览量' AFTER `content`;
实体:
private Integer views;

1、设计

先来定义一下,何为浏览,即,用户点击了商品详情的时候视为一次浏览。

所以按照上面的说法,是不是每次用户点击了商品详情就给数据库字段 1 即可,就像下面这样:

UPDATE sb_commodity SET views = views   1 WHERE id = ?

理论上,这个做法是可以的,但是这不是实现浏览量功能的全部,这仅仅只是最后一步罢了。为什么这么说,大家看我下面的思考:

  1. 用户每刷新一次商品详情,update 语句就执行一次吗?
  2. 如果用户未登录,查看商品详情是否需要给浏览量 1?

总结一下就是,是否每次刷新页面,浏览量就 1 和用户未登录,浏览量是否需要 1。当然,由这两个考虑我们又可以引申出很多个问题:

  1. 浏览量 1 每次都 update 数据库吗
  2. 如果不是每次刷新一次浏览量就 1,那如何区分用户是否已经浏览了
  3. 如果按用户 id 来区分是否浏览,那未登录的用户呢
  4. 如果按照用户的 IP 来区分是否浏览,那该如何存呢
  5. 用户 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、实践

终于到动手编码环节了,此次我们需要改动的点有如下三块:

  1. 查看商品时,增加浏览量记录功能
  2. 获取商品列表时,回查商品浏览量功能
  3. 定时同步 Redis 中的浏览量到 MySQL 中

2.1 Redis 添加浏览量记录

Redis 中我们的 key 结构为:前缀 商品 ID,value 为用户 IP。

在用户查看商品信息的时候,我们记录一下该商品的浏览量,也即把下面的方法,放入查看商品信息之中。

// 添加商品浏览量
addViews(commodity.getId());

实现:

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 中缓存的浏览量。所以在获取商品列表的时候,我们需要补充一下商品的浏览量,具体实现如下。

// 回填一下 redis 中的商品浏览量
fillViews(page.getRecords());

实现:

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 也不宜一直占用过多的内存,所以这个数据落盘是一定要做的。

具体实现步骤:

  1. 获取商品浏览量的所有 key
  2. 循环获取出 key 对应的 value 值
  3. 删除所有 key
  4. 遍历并分解 key 中的商品 id ,将商品的原有浏览量和获取 Redis 的浏览量相加,存入数据库,完成同步操作

编码之前,在来看看该功能的详细流程图:

学新通

该功能的需要注意的点我已经写在了流程图中,下面就开始编码吧!

1)schedule 编写

位置:cn.j3code.merchant.schedule

@Slf4j
@Component
@AllArgsConstructor
public 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 编写

位置:

public interface CommodityService extends IService<Commodity> {
    void fillCommodityViews();
}

@Slf4j
@AllArgsConstructor
@Service
public 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 尽量放在一个事务中执行,防止出现执行失败,丢失部分浏览量或者多计算浏览量的情况。

那,本片内容就到此结束了,咱们下回见。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgbaceb
系列文章
更多 icon
同类精品
更多 icon
继续加载