【SpringBoot】⭐️整合 Redis 实现百万级数据实时排序

目录

🍸前言

🍻一、环境准备

🍺二、具体实现

🍹三、测试

👋四、章末


🍸前言

        小伙伴们大家好,上篇文章本地实践了如何在 SpringBoot 中整合支付宝的支付功能,包括支付请求的发起和支付消息的回调(如何接收外部服务的接口回调),文章链接如下:

【SpringBoot】✈️本地集成支付宝支付功能_bizcontent框架-优快云博客

        (部署到服务器上的,可以正常接收支付宝回调请求,可以参考参考呦) 

        参考之前:

        之后:

        这次也是基于 SpringBoot 去实现大量数据下的排序功能,使用的数据库是非关系型数据库 Redis, 这就需要本地环境已经有可以使用的 redis 服务器,以及 java 环境,本地使用的是 1.8 版本,测试的数据量在一百万条,具体步骤如下:

🍻一、环境准备

        java 环境的搭建,redis 的安装启动可以自行搜索教程

        场景构建:

                提供一个支持百万级数据量的实时商品排行,展示热度最高的商品

        支持功能:

                除了展示产品以外还要有以下功能:

                        添加商品到排行榜、从排行榜中移除指定商品、查询某件商品的排名以及评分、获取评分最高的前 n 个商品

        场景分析:

                在大数据量的情况下,传统的关系型数据库不太适合做频繁的查询修改操作,针对此种场景,很好想到使用缓存工具 redis 中的数据结构,Sorted Set(ZSet) 因为该数据结构本身的特性就是针对该 key 可以有一个 score 值,并且支持有序,再加上缓存的特性,整体的查询和修改操作都很高效

🍺二、具体实现

        2.1 依赖引入

                SpringBoot 集成三方工具基本就是,先引入对应的 sdk ,然后有需要的就配置下参数信息,集成 redis 亦是如此
                首先在 pom.xml 文件中添加以下代码,刷新 maven 会自动下载对应的依赖

        <!-- Spring Boot Starter for Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        2.2 配置参数

                依赖下载好之后,需要先启动好服务器,然后在配置文件中添加链接服务器所需要的参数,一般是地址加用户密码,redis 服务器默认配置是占用 6379 端口,本地配置如下:

·                由于是本地启动的服务器,整体测试也是在本地,所以直接使用 localhost 或者 127.0.0.1 即可,端口号就是 6379, 用户密码,这里指定了项目使用的数据库是哪一个(redis 默认有16个数据库,从0到15),这里的6就对应的 db6 库,超时时间也可以设置下

        2.3 代码实现

                2.3.1 实体类 

                定义一个实体类用来对应商品对象(代码中并没有用到该实例,只是具体化下商品的属性)

import lombok.Data;

@Data
public class Product {
    private String id;
    private String name;
}

                 2.3.2 商品操作业务类

                注入 redis 的操作工具,通过提供的 ops 方法来操作数据,提供数据的操作功能

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Set;

@Service
public class ProductService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    //指定热门商品缓存使用的 key
    private static final String HOT_PRODUCTS_KEY = "hot_products_key";

    //添加商品到排行中
    public void addProductToRanking(String productId, double score) {
        redisTemplate.opsForZSet().add(HOT_PRODUCTS_KEY, productId, score);
    }

    //删除指定商品数据
    public void removeProductFromRanking(String productId) {
        redisTemplate.opsForZSet().remove(HOT_PRODUCTS_KEY, productId);
    }

    //获取对应商品的当前排名
    public Long getProductRank(String productId) {
        return redisTemplate.opsForZSet().reverseRank(HOT_PRODUCTS_KEY, productId);
    }

    //获取对应商品的分数
    public Double getProductScore(String productId) {
        return redisTemplate.opsForZSet().score(HOT_PRODUCTS_KEY, productId);
    }

    //获取前 n 个热门商品
    public Set<String> getTopProducts(int count) {
        Set<String> products = redisTemplate.opsForZSet().reverseRange(HOT_PRODUCTS_KEY, 0, count - 1);
        return products;
    }
}

                2.3.3 暴露接口

                在控制层提供对应的操作接口,简单对应业务方法中的方法,如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Set;

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @PostMapping("/{productId}/rank/{score}")
    public void addProductToRanking(@PathVariable String productId, @PathVariable double score) {
        productService.addProductToRanking(productId, score);
    }

    @DeleteMapping("/{productId}/rank")
    public void removeProductFromRanking(@PathVariable String productId) {
        productService.removeProductFromRanking(productId);
    }

    @GetMapping("/{productId}/rank")
    public Long getProductRank(@PathVariable String productId) {
        return productService.getProductRank(productId);
    }

    @GetMapping("/{productId}/score")
    public Double getProductScore(@PathVariable String productId) {
        return productService.getProductScore(productId);
    }

    @GetMapping("/top/{count}")
    public Set<String> getTopProducts(@PathVariable int count) {
        return  productService.getTopProducts(count);
    }
}

                2.3.4 数据准备

                准备数据,正常场景是从数据库中查询并放到缓存中去(缓存预热),但是本地没有这么多可用数据,就简单手动创建了,创建过程如下:

        整体过程就是使用 for 循环创建一百万条数据对应的是商品信息,但是为了提升效率,增加了以下步骤:

        多线程执行:

                使用 Executors 快速创建一个有10个核心线程的线程池(生产环境不建议使用此工具创建,还是需要手动指定线程池的几个关键参数)

        批量操作:

                为了防止短时间内创建过多任务,导致内存用尽,可以批量操作,以1000个任务为单位,创建了有 1000 条任务的时候,停下创建任务的线程,等待线程池将目前积攒的任务处理完,释放出空间,再进行后续任务的创建,如果不是规整的条数,最后可以有一个单独的处理,会将最后的任务执行完;等所有任务执行完之后,销毁线程池,也可以回收资源

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

@Component
public class TestDataGenerator {

    @Autowired
    private ProductService productService;

    public void generateTestData() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        List<Future<?>> futures = new ArrayList<>();

        for (int i = 1; i <= 1_000_000; i++) {
            final int productIdIndex = i;
            futures.add(executorService.submit(() -> {
                String productId = "product_" + productIdIndex;
                double score = Math.random() * 10000;
                productService.addProductToRanking(productId, score);
            }));

            // 每1000个任务提交一次,避免内存过度占用
            if (i % 1000 == 0) {
                try {
                    // 等待当前批次的任务执行完成
                    for (Future<?> future : futures) {
                        future.get();
                    }
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
                futures.clear();
            }
        }

        // 等待最后一批任务执行完成
        for (Future<?> future : futures) {
            try {
                future.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }

        //任务执行完之后,销毁线程池,释放资源
        executorService.shutdown();
    }
}

                执行该段创建数据的代码有很多种方式,可以参考之前的文章,链接如下,这里就选用在启动类上实现 CommandLineRunner 接口,重写 run 方法,将生成数据的方法放到方法体中,这样在项目启动后会自动执行


【项目启动执行指定代码】⭐️通过案例测试下常用的实现方式_项目启动 执行代码-优快云博客

public class Main implements CommandLineRunner {

    @Autowired
    private TestDataGenerator testDataGenerator;

    public static void main(String[] args) {

        SpringApplication.run(Main.class,args);

    }


    @Override
    public void run(String... args) throws Exception {
        long begin = System.currentTimeMillis();
        //项目启动初始化数据
        System.out.println("缓存加载开始");
        testDataGenerator.generateTestData();
        System.out.println("缓存初始化耗时:"+ (System.currentTimeMillis() - begin));
    }
}

 🍹三、测试

        3.1 启动项目先观察下数据的创建情况,包括时间,和数据量是否对得上,通过日志可以看到多线程状态下创建一百万条数据耗时 24 秒,通过 redis 可视化客户端可以看到数据整整齐齐的一百万条,也都有对应的 score 值

        3.2 数据都没问题后,可以测试下接口

                3.2.1 先获取评分前十的商品,这里使用的插件是 coolRequest 点击控制层对应的方法左侧的快捷键,可以快速生成对应的请求,只需要手动填写参数即可;到管理端上校验下数据是否对的上

 

                 3.2.2 获取指定商品的分数和排名,这里就用第一条数据来做校验

                3.2.3 新增商品 

                新增一条商品数据,对应的分值为10000,目前的最高分数,排行应该是第一,到管理端看下

                3.2.4 删除商品数据 

                这里就删除之前的排行第一商品,看下数据

                测试后可以看到接口都正常,并且效率还可以,总结如下:

👋四、章末

        当然,本地只是简单模拟下这种场景,具体的实现还要看实际生产需求,但是缓存的引入和多线程的数据处理以及缓存预热的方式都可以参考

        文章到这里就结束了 ~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

先锋 Coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值