Elasticsearch Java实战手册:搭建、条件构建与分页优化

Elasticsearch(简称ES)作为分布式搜索与分析引擎,在Java后端体系中占据重要地位。本文摒弃零散知识点堆砌,以"实战落地"为核心,分为ES环境搭建与配置、核心查询条件构建、大数据量分页优化三大部分,全程配套可直接复用的代码,助力开发者快速解决实际问题。

第一部分:ES环境搭建与Java客户端配置

环境搭建是ES开发的基础,本节涵盖ES服务部署核心要点与Java客户端配置全流程,基于ES 8.x版本(当前稳定版)展开,该版本默认开启安全认证,需特别注意配置细节。

1.1 ES服务部署核心配置

ES服务部署支持单机与集群模式,此处以单机部署为例,核心配置文件(elasticsearch.yml)关键参数如下,集群模式可在此基础上扩展节点配置:

java 复制代码
# 集群名称(客户端连接时需一致)
cluster.name: my-elasticsearch-cluster
# 节点名称
node.name: node-1
# 绑定地址(允许外部访问需设为0.0.0.0)
network.host: 0.0.0.0
# HTTP端口(默认9200,客户端连接端口)
http.port: 9200
# 传输端口(集群节点通信,默认9300)
transport.port: 9300
# 初始化主节点
cluster.initial_master_nodes: ["node-1"]
# 安全认证配置(ES 8.x默认开启)
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true

配置完成后启动ES服务,首次启动需执行初始化密码命令(生成elastic用户密码,客户端连接时使用):

bash 复制代码
# Linux/Mac环境
bin/elasticsearch-setup-passwords interactive
# Windows环境
bin\elasticsearch-setup-passwords.bat interactive

1.2 Java客户端依赖与初始化

Java开发推荐使用官方High Level REST Client,封装了高频API,易用性远超低级客户端。

1.2.1 依赖配置(Maven)

需确保客户端版本与ES服务端版本完全一致,避免兼容性问题:

java 复制代码
<!-- ES高级REST客户端 -->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>8.10.4</version>
</dependency>
<!-- ES核心依赖 -->
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>8.10.4</version>
</dependency>
<!-- JSON解析依赖(用于结果映射) -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.3</version>
</dependency>
1.2.2 客户端单例封装

ES客户端创建成本较高,需封装为单例模式,避免频繁创建连接。同时处理安全认证、超时等核心配置:

java 复制代码
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import java.io.IOException;

/**
 * ES客户端工具类(单例模式)
 */
public class EsClientFactory {
    // 单例实例
    private static volatile RestHighLevelClient client;
    // ES配置常量(实际项目建议放在配置文件中)
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 9200;
    private static final String SCHEME = "http";
    private static final String USERNAME = "elastic";
    private static final String PASSWORD = "你的ES密码";

    // 私有构造方法(禁止外部实例化)
    private EsClientFactory() {}

    /**
     * 获取客户端实例
     */
    public static RestHighLevelClient getClient() {
        if (client == null) {
            synchronized (EsClientFactory.class) {
                if (client == null) {
                    // 配置安全认证
                    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
                    credentialsProvider.setCredentials(AuthScope.ANY,
                            new UsernamePasswordCredentials(USERNAME, PASSWORD));

                    // 构建客户端
                    client = new RestHighLevelClient(
                            RestClient.builder(new HttpHost(HOST, PORT, SCHEME))
                                    .setHttpClientConfigCallback(httpClientBuilder ->
                                            httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider))
                                    // 可选:设置超时时间
                                    .setRequestConfigCallback(requestConfigBuilder ->
                                            requestConfigBuilder.setConnectTimeout(5000)
                                                    .setSocketTimeout(10000))
                    );
                }
            }
        }
        return client;
    }

    /**
     * 关闭客户端(应用销毁时调用)
     */
    public static void closeClient() throws IOException {
        if (client != null) {
            client.close();
            client = null;
        }
    }
}

第二部分:核心查询条件构建(QueryBuilders全解析)

QueryBuilders是ES Java客户端的查询条件构建核心工具类,开发者常因方法众多而混淆。本节先通过表格清晰区分高频方法的作用与适用场景,再通过一个综合代码示例集中演示用法。

2.1 高频QueryBuilders方法对比表

下表涵盖开发中最常用的查询方法,重点标注各方法的核心差异与适用场景:

查询方法 核心作用 适用字段类型 关键特性 适用场景
termQuery 单值精确匹配 keyword、数字、日期、布尔 不分词,完全匹配字段原始值 根据状态(如status=1)、ID查询
termsQuery 多值精确匹配 keyword、数字、日期、布尔 匹配字段值在指定集合中,类似SQL的IN 多ID查询(如userId in [1001,1002])
matchQuery 标准全文检索 text 对查询词分词,匹配任意分词结果,支持相关性评分 商品模糊搜索(如"小米手机")
matchPhraseQuery 短语精确匹配 text 对查询词分词,要求分词连续且顺序一致 搜索完整短语(如"Java开发")
wildcardQuery 通配符模糊匹配 text、keyword 支持*(任意字符)和?(单个字符),不依赖分词器 前缀/后缀匹配(如"张*""138?5*")
matchAllQuery 匹配所有文档 任意类型 无查询条件,需配合分页使用 全量数据导出、索引文档统计
rangeQuery 范围匹配 数字、日期、字符串 支持gt(>)、gte(≥)、lt(<)、lte(≤)等条件 价格区间、时间范围查询
geoBoundingBoxQuery 地理边界框查询 geo_point(经纬度) 查询位于指定矩形区域内的地理数据 周边商家、地图POI筛选

2.2 综合代码示例:多查询方法实战

以下示例基于"电商商品索引(product_index)",该索引包含商品ID、名称、分类、价格、上架时间、商家经纬度等字段,集中演示上述查询方法的使用,配合布尔查询(must/should)实现多条件组合:

2.2.1 索引映射(Mapping)参考

先明确索引结构,确保字段类型与查询方法匹配:

java 复制代码
{
  "mappings": {
    "properties": {
      "productId": { "type": "keyword" }, // 商品ID(精确匹配)
      "productName": { "type": "text", "analyzer": "ik_max_word" }, // 商品名称(全文检索)
      "categoryId": { "type": "integer" }, // 分类ID(精确/范围匹配)
      "price": { "type": "double" }, // 价格(范围匹配)
      "createTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }, // 上架时间(范围匹配)
      "merchantLocation": { "type": "geo_point" } // 商家经纬度(地理查询)
    }
  }
}
2.2.2 综合查询代码
java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import java.io.IOException;

/**
 * QueryBuilders综合查询示例
 * 需求:查询"分类ID为10或20、价格在100-500元、名称包含'无线'且不是'无线鼠标'、2024年上架、位于北京某区域内"的商品
 */
public class EsQueryComprehensiveDemo {
    // JSON解析工具(用于结果映射)
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    public static void main(String[] args) {
        RestHighLevelClient client = null;
        try {
            // 1. 获取客户端实例
            client = EsClientFactory.getClient();

            // 2. 构建布尔查询(多条件组合核心)
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

            // 2.1 must条件:必须满足(核心筛选条件)
            // 价格范围:100 ≤ price ≤ 500(rangeQuery)
            boolQuery.must(QueryBuilders.rangeQuery("price")
                    .gte(100.0)
                    .lte(500.0));
            // 上架时间:2024年1月1日之后(rangeQuery)
            boolQuery.must(QueryBuilders.rangeQuery("createTime")
                    .gte("2024-01-01 00:00:00"));
            // 商品名称包含"无线"(matchQuery,全文检索)
            boolQuery.must(QueryBuilders.matchQuery("productName", "无线"));

            // 2.2 should条件:满足任意一个(可选筛选,提升相关性)
            // 分类ID为10或20(termsQuery,多值精确匹配)
            boolQuery.should(QueryBuilders.termsQuery("categoryId", 10, 20));
            // 设置should最小匹配数(确保至少满足一个)
            boolQuery.minimumShouldMatch(1);

            // 2.3 must_not条件:排除(禁止出现的条件)
            // 商品名称不是"无线鼠标"(matchPhraseQuery,短语精确匹配)
            boolQuery.mustNot(QueryBuilders.matchPhraseQuery("productName", "无线鼠标"));

            // 2.4 filter条件:过滤(不影响评分,提升性能)
            // 商家位于北京某矩形区域内(geoBoundingBoxQuery,地理查询)
            GeoPoint topLeft = new GeoPoint(39.93, 116.38); // 矩形左上角经纬度
            GeoPoint bottomRight = new GeoPoint(39.90, 116.42); // 矩形右下角经纬度
            boolQuery.filter(QueryBuilders.geoBoundingBoxQuery("merchantLocation")
                    .setCorners(topLeft, bottomRight));

            // 3. 构建查询源(配置分页、排序、返回字段等)
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            sourceBuilder.query(boolQuery);
            // 分页:第1页,每页10条(from从0开始)
            sourceBuilder.from(0);
            sourceBuilder.size(10);
            // 排序:按价格升序、上架时间降序
            sourceBuilder.sort("price", SortOrder.ASC);
            sourceBuilder.sort("createTime", SortOrder.DESC);
            // 只返回需要的字段(减少数据传输)
            sourceBuilder.fetchSource(new String[]{"productId", "productName", "price", "createTime"}, null);

            // 4. 构建查询请求(指定索引)
            SearchRequest searchRequest = new SearchRequest("product_index");
            searchRequest.source(sourceBuilder);

            // 5. 执行查询并处理结果
            SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
            handleSearchResult(response);

        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("ES查询异常:" + e.getMessage());
        } finally {
            // 6. 关闭客户端(实际项目中建议在应用销毁时统一关闭)
            if (client != null) {
                try {
                    EsClientFactory.closeClient();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 处理查询结果,映射为Java对象
     */
    private static void handleSearchResult(SearchResponse response) throws IOException {
        // 总命中数
        long totalHits = response.getHits().getTotalHits().value;
        System.out.println("查询总命中商品数:" + totalHits);

        // 遍历结果并映射为Product对象
        for (var hit : response.getHits()) {
            Product product = OBJECT_MAPPER.readValue(hit.getSourceAsString(), Product.class);
            System.out.printf("商品ID:%s,名称:%s,价格:%.2f,上架时间:%s%n",
                    product.getProductId(), product.getProductName(),
                    product.getPrice(), product.getCreateTime());
        }
    }

    /**
     * 商品实体类(与索引字段对应)
     */
    static class Product {
        private String productId;
        private String productName;
        private Integer categoryId;
        private Double price;
        private String createTime;

        // getter和setter方法(省略,实际开发需补充)
        public String getProductId() { return productId; }
        public void setProductId(String productId) { this.productId = productId; }
        public String getProductName() { return productName; }
        public void setProductName(String productName) { this.productName = productName; }
        public Double getPrice() { return price; }
        public void setPrice(Double price) { this.price = price; }
        public String getCreateTime() { return createTime; }
        public void setCreateTime(String createTime) { this.createTime = createTime; }
    }
}

2.3 核心注意事项

  1. 字段类型匹配:text类型字段不可用termQuery(会因分词导致匹配失败),keyword/数字字段优先用term/termsQuery; 2. 性能优化:纯筛选场景用filter上下文(boolQuery.filter())替代must,避免计算相关性评分; 3. 地理查询前提:geo_point字段需正确存储经纬度(格式为[经度,纬度]或"纬度,经度"); 4. 通配符使用:wildcardQuery避免将*放在开头(如*无线),会导致全表扫描,性能极差。

第三部分:大数据量分页查询优化(几十万条数据场景)

当ES索引数据量达到几十万甚至上百万条时,传统的"from+size"分页会面临严重的性能问题,甚至出现内存溢出。本节先分析传统分页的缺陷,再给出两种工业级优化方案。

3.1 传统分页(from+size)的缺陷

日常开发中最常用的分页方式是通过SearchSourceBuilder的from()和size()方法设置,例如from=1000,size=10表示查询第101页(每页10条)的数据。其核心缺陷在于:

  1. 数据重复加载:ES为获取第101页数据,需要在每个分片上加载前1010条数据(1000+10),然后汇总到协调节点进行排序和截断,最终只返回10条数据,大量数据加载和排序造成资源浪费;

  2. 深度分页性能暴跌:当from值超过10000时,ES默认会抛出异常(index.max_result_window默认值为10000),即使修改该参数,from值越大,性能下降越明显;

  3. 内存溢出风险:分片加载大量数据到内存进行排序,容易触发OOM。

结论:from+size仅适用于分页深度较浅的场景(如前100页),不适用于几十万条数据的深度分页。

3.2 优化方案一:Scroll分页(滚动分页)

Scroll分页是ES专门为"全量数据导出"或"深度分页"设计的方案,其核心原理是:查询时生成一个临时的"滚动上下文(Scroll Context)",记录当前查询的分片位置和排序状态,后续分页仅需通过滚动ID(scroll_id)获取下一批数据,避免重复加载和排序。

3.2.1 适用场景

全量数据导出(如每日同步ES数据到数据库)、无需跳页的深度分页(如日志按时间顺序滚动查看)。

3.2.2 实现步骤与代码

Scroll分页分为"初始化滚动查询"和"循环获取下一批数据"两个阶段,需注意滚动上下文有过期时间(避免资源泄露):

java 复制代码
import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.common.unit.TimeValue;

/**
 * Scroll分页优化示例(适用于全量数据导出/深度无跳页分页)
 */
public class EsScrollPaginationDemo {
    // 滚动上下文过期时间(建议设置为1-5分钟,避免过长占用资源)
    private static final TimeValue SCROLL_TIMEOUT = TimeValue.timeValueMinutes(2);
    // 每页数据量
    private static final int PAGE_SIZE = 1000;

    public static void main(String[] args) {
        RestHighLevelClient client = null;
        String scrollId = null;
        try {
            client = EsClientFactory.getClient();

            // 1. 初始化滚动查询(获取第一页数据和scrollId)
            SearchRequest initRequest = new SearchRequest("product_index");
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            // 查询条件(可根据需求调整)
            sourceBuilder.query(QueryBuilders.matchAllQuery());
            sourceBuilder.size(PAGE_SIZE);
            // 按productId升序排序(Scroll分页必须指定排序字段,避免数据重复/遗漏)
            sourceBuilder.sort("productId", SortOrder.ASC);
            // 启用滚动查询
            initRequest.scroll(SCROLL_TIMEOUT);
            initRequest.source(sourceBuilder);

            // 执行初始化查询
            SearchResponse initResponse = client.search(initRequest, RequestOptions.DEFAULT);
            // 获取滚动ID(后续分页核心)
            scrollId = initResponse.getScrollId();
            // 处理第一页数据
            long totalHits = initResponse.getHits().getTotalHits().value;
            System.out.println("总数据量:" + totalHits);
            handleSearchResult(initResponse);

            // 2. 循环获取下一批数据(直到无数据返回)
            while (initResponse.getHits().getHits().length > 0) {
                // 构建滚动查询请求
                SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
                scrollRequest.scroll(SCROLL_TIMEOUT);

                // 执行滚动查询
                SearchResponse scrollResponse = client.searchScroll(scrollRequest, RequestOptions.DEFAULT);
                // 更新scrollId(部分场景下ES会返回新的scrollId)
                scrollId = scrollResponse.getScrollId();
                // 处理当前页数据
                handleSearchResult(scrollResponse);
            }

            // 3. 数据获取完成,清除滚动上下文(释放资源,必须执行)
            ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
            clearScrollRequest.addScrollId(scrollId);
            client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
            System.out.println("Scroll分页完成,已释放滚动上下文");

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (client != null) {
                try {
                    EsClientFactory.closeClient();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 结果处理方法(复用前文的handleSearchResult)
    private static void handleSearchResult(SearchResponse response) throws IOException {
        // 实现同前文,此处省略
    }
}
3.2.3 Scroll分页注意事项
  • 必须指定排序字段:建议使用唯一字段(如productId、_id)排序,避免数据重复或遗漏;

  • 及时释放资源:数据获取完成后必须调用clearScroll清除滚动上下文,否则ES会持续占用资源;

  • 不支持跳页:Scroll分页仅能按顺序获取数据,无法直接跳转到第100页,适合无跳页场景。

3.3 优化方案二:Search After分页(基于上一页最后一条数据的排序值)

Search After是ES 5.0+推出的分页方案,核心原理是:基于上一页最后一条数据的"排序字段值"作为查询条件,获取下一页数据,完全避免了from参数带来的性能问题,支持跳页(需记录中间页的排序值)。

3.3.1 适用场景

需要跳页的深度分页场景(如电商平台的商品列表分页)、大数据量下的分页查询,支持实时数据(Scroll分页依赖快照,无法获取查询后的新增数据)。

3.3.2 实现条件与代码

实现前提:必须指定唯一的排序字段(如_id或业务唯一字段+_id),确保每条数据的排序值唯一,避免分页遗漏。

java 复制代码
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.sort.SortField;

/**
 * Search After分页优化示例(适用于需跳页的深度分页场景)
 * 需求:查询第5页商品数据(每页10条),基于productId升序排序
 */
public class EsSearchAfterPaginationDemo {
    // 每页数据量
    private static final int PAGE_SIZE = 10;
    // 目标页码(从1开始)
    private static final int TARGET_PAGE = 5;

    public static void main(String[] args) {
        RestHighLevelClient client = null;
        try {
            client = EsClientFactory.getClient();

            // 排序字段配置(必须包含唯一字段,此处用productId)
            String[] sortFields = {"productId"};
            SortOrder[] sortOrders = {SortOrder.ASC};

            // 存储上一页最后一条数据的排序值(初始为null,即第一页)
            Object[] lastSortValues = null;

            // 循环获取到目标页码
            for (int page = 1; page <= TARGET_PAGE; page++) {
                SearchRequest searchRequest = new SearchRequest("product_index");
                SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

                // 查询条件(可根据需求调整)
                sourceBuilder.query(QueryBuilders.rangeQuery("price").lte(1000.0));
                // 分页配置:size固定为PAGE_SIZE,from必须设为0(Search After不依赖from)
                sourceBuilder.size(PAGE_SIZE);
                sourceBuilder.from(0);

                // 设置排序(与上一页一致)
                for (int i = 0; i < sortFields.length; i++) {
                    sourceBuilder.sort(sortFields[i], sortOrders[i]);
                }

                // 设置Search After条件(非第一页时添加)
                if (lastSortValues != null) {
                    sourceBuilder.searchAfter(lastSortValues);
                }

                // 执行查询
                SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

                // 处理当前页数据(仅目标页需要详细处理,其他页仅记录lastSortValues)
                if (page == TARGET_PAGE) {
                    System.out.printf("第%d页数据:%n", TARGET_PAGE);
                    handleSearchResult(response);
                }

                // 更新lastSortValues(获取当前页最后一条数据的排序值)
                SearchHit[] hits = response.getHits().getHits();
                if (hits.length == 0) {
                    System.out.println("已无更多数据");
                    break;
                }
                lastSortValues = hits[hits.length - 1].getSortValues();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (client != null) {
                try {
                    EsClientFactory.closeClient();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 结果处理方法(复用前文的handleSearchResult)
    private static void handleSearchResult(SearchResponse response) throws IOException {
        // 实现同前文,此处省略
    }
}
优势:
  • 性能优异:无需加载前N条数据,仅基于上一页排序值查询,支持无限深度分页;

  • 支持实时数据:查询基于最新数据,不会像Scroll那样依赖快照;

  • 支持跳页:只要记录中间页的lastSortValues,即可跳转到任意页。

注意事项:
  • 排序字段唯一:必须使用唯一字段(如_id)作为排序条件之一,否则可能出现数据重复或遗漏;

  • 需记录排序值:跳页时需保存上一页的lastSortValues,建议存储在前端或缓存中;

  • 不支持总条数:Search After无法直接获取总数据量(需单独查询),适合无需显示总页数的场景。

3.4 分页方案对比与选型建议

分页方案 性能(深度分页) 支持跳页 支持实时数据 适用场景
from+size 差(from越大越慢) 浅分页(前100页)、需显示总页数
Scroll 中(基于快照,稳定) 全量数据导出、无跳页深度分页
Search After 优(无性能衰减) 是(需记录排序值) 需跳页的深度分页、实时数据查询

选型口诀:浅分页用from+size,全量导出用Scroll,深度跳页用Search After; 特殊场景:若需显示总页数且分页深度较大,可结合"from+size获取前100页+Search After获取深度页"的混合方案。

总结

本文从ES环境搭建、核心查询条件构建到大数据量分页优化,形成了完整的Java实战链路。重点在于:

  1. 环境配置需注意ES 8.x的安全认证,客户端采用单例模式避免资源浪费;

  2. 查询条件构建的核心是"字段类型与查询方法匹配",通过表格对比可快速区分易混淆方法;

  3. 大数据量分页需摒弃传统的from+size,根据是否需要跳页选择Scroll或Search After方案。

ES的Java开发本质是"场景匹配"------结合业务场景选择合适的API与优化方案,才能充分发挥其高性能优势。后续可进一步学习聚合查询、高亮查询等高级特性,构建更完整的ES技术体系。

相关推荐
期待のcode7 分钟前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐9 分钟前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
a程序小傲19 分钟前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红20 分钟前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥21 分钟前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v32 分钟前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地44 分钟前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209251 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei1 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot
记得开心一点嘛2 小时前
Redis封装类
java·redis