实现商场项目中的热门数据分离

109 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

每日英语:

Rather than love,than money,than fame,give me the truth.

我不要爱情、金钱和荣誉,给我真理。 -亨利·大卫·梭罗

1610438262742.png 如上图,是我们在商场项目中的热门商品收集和分析流程,而收集已经实现,现在需要实现的是热门商品分析。

1. 热门商品分析

1610691494237.png 热门商品分析我们可以在mall-seckill-service中使用elastic-job定时每10分钟查询mall-dw-service,将每小时内访问次数超过1万的商品定为热点商品,再将热点商品从数据库中查询出来并填充到Redis,但此时需要考虑到用户下单安全问题。

1.1 热门商品查询分析

我们需要编写一个方法,满足每小时商品访问频率超过1万的商品,我们下面写了个SQL语句:

SELECT 
    uri,count(*) as viewCount 
FROM msitemslog 
WHERE 
    __time>=TIMESTAMP '2023-2-4 14:27:37' 
AND 
    uri NOT IN ('/msitems/100001956475.html','/msitems/1.html') 
GROUP BY uri 
HAVING viewCount>10000 
ORDER BY viewCount desc 
LIMIT 100

我们对上面这个语句进行分析:

SELECT 
    uri,count(*) as viewCount 
FROM msitemslog 
WHERE 
    --【最近1小时内商品访问频率,时间=当前时间-1小时】
    __time>=TIMESTAMP '2023-2-4 14:27:37' 
AND 
    --【排除已经是热点商品的数据查询】
    uri NOT IN ('/msitems/100001956475.html','/msitems/1.html') 
--【统计每个商品访问的次数,而每个商品访问的uri其实就是区分不同商品,所以根据uri分组】
GROUP BY uri 
--【访问次数>1万的商品才是热门商品】
HAVING viewCount>10000 
--【根据访问次数倒序】
ORDER BY viewCount desc 
--【查询商品个数不要太多,如果太多可以做分页查询】
LIMIT 100

1.2 热门商品查询

按照上面的分析编写查询接口。

1)Dao

修改com.xz.mall.dw.mapper.HotGoodsMapper增加查询热点商品方法:

/****
 * 分组、聚合判断、TopN、时间判断、排序
 * @param size
 * @param time
 * @param urls
 * @param max
 * @return
 */
@Select("SELECT uri,count(*) as viewCount FROM msitemslog WHERE __time>=TIMESTAMP '${time}' AND uri NOT IN ('${urls}') GROUP BY uri HAVING viewCount>#{max} ORDER BY viewCount desc LIMIT #{size}")
List<Map<String,String>> searchHotGoods(@Param("size") Integer size,
                                        @Param("time") String time,
                                        @Param("urls")String urls,
                                        @Param("max")Integer max);

2)Service

接口:修改com.xz.mall.dw.service.HotGoodsService添加查询热门商品方法

List<Map<String,String>> searchHotGoods(Integer size, Integer hour, String[] urls, Integer max);

实现类:修改com.xz.mall.dw.service.impl.HotGoodsServiceImpl实现接口方法

/***
 * 热门商品查询
 * @param size:TopN
 * @param hour:N小时前数据统计
 * @param urls:排除之前判断的热点商品
 * @param max:访问频率超过max作为统计条件
 * @return
 */
@Override
public List<Map<String,String>> searchHotGoods(Integer size, Integer hour, String[] urls, Integer max) {
    String urlsJoin = StringUtils.join(urls, "','");
    return hotGoodsMapper.searchHotGoods(size, TimeUtil.beforeTime(TimeUtil.unit_hour,hour),urlsJoin,max);
}

3)Controller

修改com.xz.mall.dw.controller.HotGoodsController实现方法调用

/****
 * 热点商品查询
 */
@PostMapping("/search/{size}/{hour}/{max}")
public RespResult<List<Map<String,String>>> searchHot(
                                            @PathVariable(value = "size")Integer size,
                                            @PathVariable(value = "hour")Integer hour,
                                            @PathVariable(value = "max")Integer max,
                                            @RequestBody(required = false) String[] ids){
    //集合查询前N条
    List<Map<String,String>> hotGoods = hotGoodsService.searchHotGoods(size,hour,ids,max);
    return RespResult.ok(hotGoods);
}

4)Feign

我们需要在mall-seckill-service中调用该方法,因此需要创建feign接口。

创建com.xz.mall.dw.feign.HotGoodsFeign代码如下:

@FeignClient(value = "mall-dw")
public interface HotGoodsFeign {
​
    /****
     * 热点商品查询
     */
    @PostMapping("/hot/goods/search/{size}/{hour}/{max}")
    public RespResult<List<Map<String,String>>> searchHot(
            @PathVariable(value = "size")Integer size,
            @PathVariable(value = "hour")Integer hour,
            @PathVariable(value = "max")Integer max,
            @RequestBody(required = false) String[] ids);
}

2 热门商品分离

我们先编写定时查询热门商品,再实现热门商品隔离,此时我们要引入mall-dw-api依赖,并且需要引入elasticjob

mall-seckill-service的pom.xml中引入依赖:

<dependencies>
    <dependency>
        <groupId>com.xz</groupId>
        <artifactId>mall-seckill-api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
​
    <!-- ElasticJobAutoConfiguration自动配置类作用-->
    <dependency>
        <groupId>com.github.kuhn-he</groupId>
        <artifactId>elastic-job-lite-spring-boot-starter</artifactId>
        <version>2.1.5</version>
    </dependency>
​
    <dependency>
        <groupId>com.xz</groupId>
        <artifactId>mall-dw-api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

2.1 定时查询

mall-seckill-service中实现定时查询druid中的数据,我们可以创建定时任务类com.xz.mall.seckill.task.DiscoverHotGoods,在类中定时查询druid数据:

@ElasticSimpleJob(
        jobName = "${elaticjob.zookeeper.namespace}",
        shardingTotalCount = 1,
        cron = "0/10 * * * * ? *"
)
@Component
public class DiscoverHotGoods implements SimpleJob {
​
    @Autowired
    private HotGoodsFeign hotGoodsFeign;
​
    //热门数据条件
    @Value("${hot.size}")
    private Integer size;
    @Value("${hot.hour}")
    private Integer hour;
    @Value("${hot.max}")
    private Integer max;
​
    @Override
    public void execute(ShardingContext shardingContext) {
        //远程调用
        String[] ids={};
        RespResult<List<Map<String, String>>> listRespResult = hotGoodsFeign.searchHot(size, hour, max, ids);
        //集合数据获取
        List<Map<String, String>> listData = listRespResult.getData();
        //结果信息
        for (Map<String, String> dataMap : listData) {
            //处理请求路径
            String uri =uriReplace( dataMap.get("uri") , 1);
            System.out.println("查询到的商品ID:"+uri);
        }
    }
​
    /***
     * uri处理
     * @param uri
     * @param type:1 uri表示商品请求路径, 2 uri表示商品ID
     * @return
     */
    public String uriReplace(String uri,Integer type){
        switch (type){
            case 1:
                uri=uri.replace("/msitems/","").replace(".html","");
                break;
            case 2:
                uri="/msitems/"+uri+".html";
                break;
            default:
                    uri="/msitems/"+uri+".html";
        }
        return uri;
    }
}

上面的时间和查询数量等信息我们可以配置在bootstrap.yml中:

spring:
  redis:
    host: 192.168.xxx.xxx
    port: 6379
elaticjob:
  zookeeper:
    server-lists: 192.168.xxx.xxx:2181
    namespace: hotsync
hot:
  #查询条数
  size: 100
  #N小时的数据
  hour: 1
  #每小时查询超过max次
  max: 1

2.2 数据分离

数据隔离也就是将数据存储到处理数据非常快的存储器中,我们选择了Redis,数据存储到Redis后,下次查询的时候,应该将他们从查询条件中排除掉。

1)Service

接口:修改mall-seckill-servicecom.xz.mall.seckill.service.SeckillGoodsService接口,添加秒杀商品隔离方法:

/***
 * 隔离
 * @param uri
 */
void isolation(String uri);

实现类:修改com.xz.mall.seckill.service.impl.SeckillGoodsServiceImpl添加接口实现方法:

@Autowired
private RedisTemplate redisTemplate;
​
/***
 * 隔离
 * @param uri(id)
 */
@Override
public void isolation(String uri) {
    //锁定
    QueryWrapper<SeckillGoods> seckillGoodsQueryWrapper = new QueryWrapper<SeckillGoods>();
    seckillGoodsQueryWrapper.eq("islock",0);
    seckillGoodsQueryWrapper.eq("id",uri);
    seckillGoodsQueryWrapper.gt("store_count",0);
    SeckillGoods seckillGoods = new SeckillGoods();
    seckillGoods.setIslock(1);
    int update = seckillGoodsMapper.update(seckillGoods, seckillGoodsQueryWrapper);
    if(update>0){
        //数据存入缓存隔离(需要控制集群环境问题,所以定时任务分片只设置成1个分片)
        seckillGoods = seckillGoodsMapper.selectById(uri);
        redisTemplate.boundHashOps("HotSeckillGoods").increment(uri,seckillGoods.getStoreCount());
    }
}

2)定时任务

定时任务中先查询已经存在Redis中的热门商品,再将ID处理好,传到searchHot方法中用于排除,代码如下:

@ElasticSimpleJob(
        jobName = "${elaticjob.zookeeper.namespace}",
        shardingTotalCount = 1,
        cron = "0/10 * * * * ? *"
)
@Component
public class DiscoverHotGoods implements SimpleJob {
​
    @Autowired
    private HotGoodsFeign hotGoodsFeign;
​
    @Autowired
    private SeckillGoodsService seckillGoodsService;
​
    @Autowired
    private RedisTemplate redisTemplate;
​
    //热门数据条件
    @Value("${hot.size}")
    private Integer size;
    @Value("${hot.hour}")
    private Integer hour;
    @Value("${hot.max}")
    private Integer max;
​
    @Override
    public void execute(ShardingContext shardingContext) {
        //远程调用
        String[] ids=isolationList();
        RespResult<List<Map<String, String>>> listRespResult = hotGoodsFeign.searchHot(size, hour, max, ids);
        //集合数据获取
        List<Map<String, String>> listData = listRespResult.getData();
        //结果信息
        for (Map<String, String> dataMap : listData) {
            //处理请求路径
            String uri =uriReplace( dataMap.get("uri") , 1);
            //隔离
            seckillGoodsService.isolation(uri);
        }
    }
​
    /***
     * 查询已经被隔离的热点商品
     */
    public String[] isolationList(){
        //获取所有已经被隔离的热门商品ID
        Set<String> ids = redisTemplate.boundHashOps("HotSeckillGoods").keys();
        String[] allids =new String[ids.size()];
        ids.toArray(allids);
        //uri地址处理
        for (int i = 0; i < allids.length; i++) {
            allids[i] = uriReplace(allids[i],2);
        }
        return allids;
    }
​
​
    /***
     * uri处理
     * @param uri
     * @param type:1 uri表示商品请求路径, 2 uri表示商品ID
     * @return
     */
    public String uriReplace(String uri,Integer type){
        switch (type){
            case 1:
                uri=uri.replace("/msitems/","").replace(".html","");
                break;
            case 2:
                uri="/msitems/"+uri+".html";
                break;
            default:
                    uri="/msitems/"+uri+".html";
        }
        return uri;
    }
}

总结

本篇主要讲述了一下商场项目中热门数据分离的具体实现。