使用redis实现商品销量排行榜

前言

需要统计某个商家商品的实时销量排行,可以使用SQL语句,根据销量字段排序,但是这个方法需要进行全表扫描,当数据量非常大的时候,效率很低

redis自带的数据结构zset是有序列表,可以结合redis更加高效的得到实时排行数据

数据准备

1. 表准备

CREATE TABLE `mall` (
  `id` bigint(20) NOT NULL,
  `name` varchar(20) DEFAULT NULL COMMENT '商品名称',
  `stock` bigint(20) DEFAULT '0' COMMENT '库存',
  `shop_id` varchar(32) DEFAULT NULL COMMENT '商家id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2. 测试数据

INSERT INTO `mall` VALUES (1, '红豆奶茶', 1000, '1001');
INSERT INTO `mall` VALUES (2, '原味奶茶', 1000, '1001');
INSERT INTO `mall` VALUES (3, '巧克力奶茶', 1000, '1001');
INSERT INTO `mall` VALUES (4, '柠檬水', 1000, '1001');
INSERT INTO `mall` VALUES (5, '双皮奶', 1000, '1001');
INSERT INTO `mall` VALUES (6, '茉莉雪顶', 1000, '1001');
INSERT INTO `mall` VALUES (7, '相思奶茶', 1000, '1001');
INSERT INTO `mall` VALUES (8, '城市恋人', 1000, '1001');
INSERT INTO `mall` VALUES (9, '冰可乐', 1000, '1001');
INSERT INTO `mall` VALUES (10, '心动之芒', 1000, '1001');
INSERT INTO `mall` VALUES (11, '藿香正气水', 1000, '1001');

工程搭建

我这里使用springboot工程,集成mybatis-plusredis

1. 相关依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.4.1</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
        <version>2.4.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.28</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.47</version>
    </dependency>
</dependencies>

2. 配置文件

server:
  port: 8001
  context-path: /
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=true
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    password:
    jedis:
      pool:
        max-active: 20
        max-wait: 1
        max-idle: 10
        min-idle: 0
    timeout: 1000
mybatis-plus:
  type-aliases-package: com.weilc.demo.entity

3. 映射文件

使用逆向工程生成实体类和mapper接口,因为这里用的mybatis-plus,所以单表操作可以不生成xml文件

逆向工程没有的也可以使用我写的这个:mybatis-plus逆向工程工具

功能测试

我这里直接使用springboot自带的redisTemplate操作redis

使用前先注入redisTemplateMallMapper

private static final String SALE_KEY = "SALE_KEY";
@Autowired
private MallMapper mallMapper;
@Autowired
private StringRedisTemplate redisTemplate;

1. 新增销量

@GetMapping("/increment/{id}/{num}")
public String increment(@PathVariable("id") String id, @PathVariable("num")double num) {
    Mall mall = mallMapper.selectById(id);
    log.info("售出-" + mall.getName());
    String key = SALE_KEY + "_" + mall.getShopId();
    redisTemplate.opsForZSet().incrementScore(key, mall.getName(), num);
    return "ok";
}
@GetMapping("/add/{id}/{num}")
public String add(@PathVariable("id") String id, @PathVariable("num")double num) {
    Mall mall = mallMapper.selectById(id);
    log.info("售出-" + mall.getName());
    String key = SALE_KEY + "_" + mall.getShopId();
    redisTemplate.opsForZSet().add(key, mall.getName(), num);
    return "ok";
}

因为实际我们数据库里面应该是有很多不同的商家,所以我们这里的key使用静态常量加上shoId代表该商家的唯一标识

这里有两个接口,都可以实现新增数据的功能,区别就是第二个接口的方法add,会覆盖掉原来key的销量的值,相当于更新操作,而第一个接口的incrementScore方法,会在原有的基础上进行累加

我们在这里传入不同的id,模拟每个商品的销售情况

2. 获取指定范围的排行(根据销量倒序)

@GetMapping("/rank/{shopId}/{start}/{end}")
public Set rank(@PathVariable("shopId")String shopId,@PathVariable("start")long start,@PathVariable("end")long end) {
    String key = SALE_KEY + "_" + shopId;
    Set<String> set = redisTemplate.opsForZSet().reverseRange(key, start, end);
    return set;
}

如:获取该商家销量前10的商品列表:http://127.0.0.1:8001/mall/rank/1001/0/10

返回数据:

["城市恋人","红豆奶茶","柠檬水","相思奶茶","茉莉雪顶","巧克力奶茶","双皮奶","藿香正气水","心动之芒","原味奶茶","冰可乐"]

3. 获取指定范围的排行和销量

@GetMapping("/rankWithScore/{shopId}/{start}/{end}")
public Set rankWithScore(@PathVariable("shopId")String shopId,@PathVariable("start")long start,@PathVariable("end")long end) {
    String key = SALE_KEY + "_" + shopId;
    Set<ZSetOperations.TypedTuple<String>> set = redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
    return set;
}

调用接口:http://127.0.0.1:8001/mall/rankWithScore/1001/0/10

返回数据:

[{"score":20.0,"value":"城市恋人"},{"score":16.0,"value":"红豆奶茶"},{"score":15.0,"value":"柠檬水"},{"score":14.0,"value":"相思奶茶"},{"score":13.0,"value":"茉莉雪顶"},{"score":11.0,"value":"巧克力奶茶"},{"score":10.0,"value":"双皮奶"},{"score":9.0,"value":"藿香正气水"},{"score":6.0,"value":"心动之芒"},{"score":3.0,"value":"原味奶茶"},{"score":2.0,"value":"冰可乐"}]

4. 查找某个商品的销量排行名次

@GetMapping("/findRank/{id}")
public Long findRank(@PathVariable String id) {
    Mall mall = mallMapper.selectById(id);
    String key = SALE_KEY + "_" + mall.getShopId();
    Long rankNum = redisTemplate.opsForZSet().reverseRank(key, mall.getName());
    return rankNum;
}

调用接口:http://127.0.0.1:8001/mall/findRank/3

返回数据:5

5. 查找某个商品的销量数字

@GetMapping("/findScore/{id}")
public Double findScore(@PathVariable String id) {
    Mall mall = mallMapper.selectById(id);
    String key = SALE_KEY + "_" + mall.getShopId();
    Double score = redisTemplate.opsForZSet().score(key, mall.getName());
    return score;
}

调用接口:http://127.0.0.1:8001/mall/findScore/3

返回数据:11.0

6. 统计某个销量区间内有多少商品

@GetMapping("/count/{shopId}/{start}/{end}")
public Long count(@PathVariable("shopId") String shopId, @PathVariable("start") long start, @PathVariable("end") long end) {
    String key = SALE_KEY + "_" + shopId;
    Long count = redisTemplate.opsForZSet().count(key, start, end);
    return count;
}

调用接口:http://127.0.0.1:8001/mall/count/1001/5/100

返回数据:9

7. 获取集合的基数(数量大小)

@GetMapping("/zCard/{shopId}")
public Long zCard(@PathVariable String shopId) {
    String key = SALE_KEY + "_" + shopId;
    Long aLong = redisTemplate.opsForZSet().zCard(key);
    return aLong;
}

调用接口:http://127.0.0.1:8001/mall/zCard/1001

返回数据:11

8. 删除指定区间排行的数据

@GetMapping("/removeRange/{shopId}/{start}/{end}")
public void clear(@PathVariable("shopId") String shopId, @PathVariable("start") long start, @PathVariable("end") long end) {
    String key = SALE_KEY + "_" + shopId;
    redisTemplate.opsForZSet().removeRange(key, start, end);
}

删掉之后,剩下的数据会重新排序

总结归纳

在上述测试中,我们了解了使用redisTemplate操作zset的添加,查询,删除等功能

1. 新增更新

//单个新增or更新
Boolean add(K key, V value, double score);
//批量新增or更新
Long add(K key, Set<TypedTuple<V>> tuples);
//使用加法操作分数
Double incrementScore(K key, V value, double delta);

2. 查询

查询分为正序和逆序,逆序只需要在下面的方法前面加上reverse即可,redis默认的是从小到大排序

列表查询

//通过排名区间获取列表值集合
Set<V> range(K key, long start, long end);
//通过排名区间获取列表值和分数集合
Set<TypedTuple<V>> rangeWithScores(K key, long start, long end);
//通过分数区间获取列表值集合
Set<V> rangeByScore(K key, double min, double max);
//通过分数区间获取列表值和分数集合
Set<TypedTuple<V>> rangeByScoreWithScores(K key, double min, double max);
//通过Range对象删选再获取集合排行
Set<V> rangeByLex(K key, Range range);
//通过Range对象删选再获取limit数量的集合排行
Set<V> rangeByLex(K key, Range range, Limit limit);

单人查询

//获取个人排行
Long rank(K key, Object o);
//获取个人分数
Double score(K key, Object o);

3. 删除

//通过key/value删除
Long remove(K key, Object... values);
//通过排名区间删除
Long removeRange(K key, long start, long end);
//通过分数区间删除
Long removeRangeByScore(K key, double min, double max);

4. 统计

//统计分数区间的人数
Long count(K key, double min, double max);
//统计集合基数
Long zCard(K key);

以上就是redis的排行榜功能实现,码字不易,觉得有用的话点个赞

上一篇:Django实现MySQL读写分离


下一篇:Github上42.4K的JAVA跑动起来