分布式实战 | 第一篇 :ELK在开源全栈项目【有来商城】的应用,不仅仅是分布式日志收集(二)

简介: 分布式实战 | 第一篇 :ELK在开源全栈项目【有来商城】的应用,不仅仅是分布式日志收集(二)


五. SpringBoot整合ElasticSearch客户端RestHighLevelClient

1. pom依赖

代码坐标: common-elasticsearch#pom.xml


客户端的版本需和服务器的版本对应,这里也就是7.10.1



   org.elasticsearch.client

   elasticsearch-rest-high-level-client

   

       

           org.elasticsearch.client

           elasticsearch-rest-client

       


       

           elasticsearch

           org.elasticsearch

       

   




   org.elasticsearch

   elasticsearch

   7.10.1




   org.elasticsearch.client

   elasticsearch-rest-client

   7.10.1




   org.springframework.boot

   spring-boot-starter-data-elasticsearch



2. yml 配置

spring:

 elasticsearch:

   rest:

     uris: ["http://localhost:9200"]

     cluster-nodes:

       - localhost:9200


3. RestHighLevelClientConfig 配置类

代码坐标: common-elasticsearch#RestHighLevelClientConfig


@ConfigurationProperties(prefix = "spring.elasticsearch.rest")

@Configuration

@AllArgsConstructor

public class RestHighLevelClientConfig {


   @Setter

   private List clusterNodes;


   @Bean

   public RestHighLevelClient restHighLevelClient() {


       HttpHost[] hosts = clusterNodes.stream()

               .map(this::buildHttpHost) // eg: new HttpHost("127.0.0.1", 9200, "http")

               .toArray(HttpHost[]::new);

       return new RestHighLevelClient(RestClient.builder(hosts));

   }


   private HttpHost buildHttpHost(String node) {

       String[] nodeInfo = node.split(":");

       return new HttpHost(nodeInfo[0].trim(), Integer.parseInt(nodeInfo[1].trim()), "http");

   }

}


4. RestHighLevelClient API封装

代码坐标: common-elasticsearch#ElasticSearchService


暂只简单封装实现需求里需要的几个方法,计数、去重计数、日期聚合统计、列表查询、分页查询、删除,后续可扩展…

@Service

@AllArgsConstructor

public class ElasticSearchService {


   private RestHighLevelClient client;


   /**

    * 计数

    */

   @SneakyThrows

   public long count(QueryBuilder queryBuilder, String... indices) {

       // 构造请求

       CountRequest countRequest = new CountRequest(indices);

       countRequest.query(queryBuilder);


       // 执行请求

       CountResponse countResponse = client.count(countRequest, RequestOptions.DEFAULT);

       long count = countResponse.getCount();

       return count;

   }


   /**

    * 去重计数

    */

   @SneakyThrows

   public long countDistinct(QueryBuilder queryBuilder, String field, String... indices) {

       String distinctKey = "distinctKey"; // 自定义计数去重key,保证上下文一致


       // 构造计数聚合 cardinality:集合中元素的个数

       CardinalityAggregationBuilder aggregationBuilder = AggregationBuilders

               .cardinality(distinctKey).field(field);


       // 构造搜索源

       SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

       searchSourceBuilder.query(queryBuilder).aggregation(aggregationBuilder);


       // 构造请求

       SearchRequest searchRequest = new SearchRequest(indices);

       searchRequest.source(searchSourceBuilder);


       // 执行请求

       SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

       ParsedCardinality result = searchResponse.getAggregations().get(distinctKey);

       return result.getValue();

   }


   /**

    * 日期聚合统计

    *

    * @param queryBuilder 查询条件

    * @param field        聚合字段,如:登录日志的 date 字段

    * @param interval     统计时间间隔,如:1天、1周

    * @param indices      索引名称

    * @return

    */

   @SneakyThrows

   public Map dateHistogram(QueryBuilder queryBuilder, String field, DateHistogramInterval interval, String... indices) {


       String dateHistogramKey = "dateHistogramKey"; // 自定义日期聚合key,保证上下文一致


       // 构造聚合

       AggregationBuilder aggregationBuilder = AggregationBuilders

               .dateHistogram(dateHistogramKey) //自定义统计名,和下文获取需一致

               .field(field) // 日期字段名

               .format("yyyy-MM-dd") // 时间格式

               .calendarInterval(interval) // 日历间隔,例: 1s->1秒 1d->1天 1w->1周 1M->1月 1y->1年 ...

               .minDocCount(0); // 最小文档数,比该值小就忽略


       // 构造搜索源

       SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

       searchSourceBuilder

               .query(queryBuilder)

               .aggregation(aggregationBuilder)

               .size(0);


       // 构造SearchRequest

       SearchRequest searchRequest = new SearchRequest(indices);

       searchRequest.source(searchSourceBuilder);


       searchRequest.indicesOptions(

               IndicesOptions.fromOptions(

                       true, // 是否忽略不可用索引

                       true, // 是否允许索引不存在

                       true, // 通配符表达式将扩展为打开的索引

                       false // 通配符表达式将扩展为关闭的索引

               ));


       // 执行请求

       SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);


       // 处理结果

       ParsedDateHistogram dateHistogram = searchResponse.getAggregations().get(dateHistogramKey);


       Iterator iterator = dateHistogram.getBuckets().iterator();


       Map map = new HashMap<>();

       while (iterator.hasNext()) {

           Histogram.Bucket bucket = iterator.next();

           map.put(bucket.getKeyAsString(), bucket.getDocCount());

       }

       return map;

   }


   /**

    * 列表查询

    */

   @SneakyThrows

   public  List search(QueryBuilder queryBuilder, Class clazz, String... indices) {

       List list = this.search(queryBuilder, null, 1, ESConstants.DEFAULT_PAGE_SIZE, clazz, indices);

       return list;

   }


   /**

    * 分页列表查询

    */

   @SneakyThrows

   public  List search(QueryBuilder queryBuilder, SortBuilder sortBuilder, Integer page, Integer size, Class clazz, String... indices) {

       // 构造SearchSourceBuilder

       SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

       searchSourceBuilder.query(queryBuilder);

       searchSourceBuilder.sort(sortBuilder);

       searchSourceBuilder.from((page - 1) * size);

       searchSourceBuilder.size(size);

       // 构造SearchRequest

       SearchRequest searchRequest = new SearchRequest(indices);

       searchRequest.source(searchSourceBuilder);

       // 执行请求

       SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

       SearchHits hits = searchResponse.getHits();

       SearchHit[] searchHits = hits.getHits();


       List list = CollectionUtil.newArrayList();

       for (SearchHit hit : searchHits) {

           T t = JSONUtil.toBean(hit.getSourceAsString(), clazz);

           t.setId(hit.getId()); // 数据的唯一标识

           t.setIndex(hit.getIndex());// 索引

           list.add(t);

       }

       return list;

   }


   /**

    * 删除

    */

   @SneakyThrows

   public boolean deleteById(String id, String index) {

       DeleteRequest deleteRequest = new DeleteRequest(index,id);

       DeleteResponse deleteResponse = client.delete(deleteRequest, RequestOptions.DEFAULT);

       return true;

   }

}


六. 后台接口

在SpringBoot整合了ElasticSearch的高级客户端RestHighLevelClient,以及简单了封装方法之后,接下来就准备为前端提供统计数据、分页列表查询记录、根据ID删除记录接口了。


1. 首页控制台

首页控制台需要今日IP访问数,历史总IP访问数、近十天每天的登录次数统计,具体代码如下:


代码坐标: youlai-admin#DashboardController


@Api(tags = "首页控制台")

@RestController

@RequestMapping("/api.admin/v1/dashboard")

@Slf4j

@AllArgsConstructor

public class DashboardController {


   ElasticSearchService elasticSearchService;


   @ApiOperation(value = "控制台数据")

   @GetMapping

   public Result data() {

       Map data = new HashMap<>();


       // 今日IP数

       long todayIpCount = getTodayIpCount();

       data.put("todayIpCount", todayIpCount);


       // 总IP数

       long totalIpCount = getTotalIpCount();

       data.put("totalIpCount", totalIpCount);


       // 登录统计

       int days = 10; // 统计天数

       Map loginCount = getLoginCount(days);

       data.put("loginCount", loginCount);


       return Result.success(data);

   }


 

   private long getTodayIpCount() {

       String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));

       TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("date", date);

       String indexName = ESConstants.LOGIN_INDEX_PATTERN + date; //索引名称

     

       // 这里使用clientIP聚合计数,为什么加.keyword后缀呢?下文给出截图

       long todayIpCount = elasticSearchService.countDistinct(termQueryBuilder, "clientIP.keyword", indexName);

       return todayIpCount;

   }


   private long getTotalIpCount() {

       long totalIpCount = elasticSearchService.countDistinct(null, "clientIP.keyword", ESConstants.LOGIN_INDEX_PATTERN);

       return totalIpCount;

   }


   private Map getLoginCount(int days) {


       LocalDateTime now = LocalDateTime.now();

       DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");


       String startDate = now.plusDays(-days).format(formatter);

       String endDate = now.format(formatter);


       String[] indices = new String[days]; // 查询ES索引数组

       String[] xData = new String[days]; // 柱状图x轴数据

       for (int i = 0; i < days; i++) {

           String date = now.plusDays(-i).format(formatter);

           xData[i] = date;

           indices[i] = ESConstants.LOGIN_INDEX_PREFIX + date;

       }


       // 查询条件,范围内日期统计

       RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("date").from(startDate).to(endDate);

       BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()

               .must(rangeQueryBuilder);



       // 总数统计

       Map totalCountMap = elasticSearchService.dateHistogram(

               boolQueryBuilder,

               "date", // 根据date字段聚合统计登录数 logback-spring.xml 中的自定义扩展字段 date

               DateHistogramInterval.days(1),

               indices);


       // 当前用户统计

       HttpServletRequest request = RequestUtils.getRequest();

       String clientIP = IPUtils.getIpAddr(request);


       boolQueryBuilder.must(QueryBuilders.termQuery("clientIP", clientIP));

       Map myCountMap = elasticSearchService.dateHistogram(boolQueryBuilder, "date", DateHistogramInterval.days(1), indices);



       // 组装echarts数据

       Long[] totalCount = new Long[days];

       Long[] myCount = new Long[days];


       Arrays.sort(xData);// 默认升序

       for (int i = 0; i < days; i++) {

           String key = xData[i];

           totalCount[i] = Convert.toLong(totalCountMap.get(key), 0l);

           myCount[i] = Convert.toLong(myCountMap.get(key), 0l);

       }

       Map map = new HashMap<>(4);


       map.put("xData", xData); // x轴坐标

       map.put("totalCount", totalCount); // 总数

       map.put("myCount", myCount); // 我的


       return map;

   }

}


聚合字段clientIP为什么添加.keyword后缀?

微信图片_20230709233623.png


2. 登录记录分页查询接口

代码坐标: youlai-admin # LoginRecordController


@Api(tags = "登录记录")

@RestController

@RequestMapping("/api.admin/v1/login_records")

@Slf4j

@AllArgsConstructor

public class LoginRecordController {


   ElasticSearchService elasticSearchService;


   ITokenService tokenService;


   @ApiOperation(value = "列表分页")

   @ApiImplicitParams({

           @ApiImplicitParam(name = "page", value = "页码", defaultValue = "1", paramType = "query", dataType = "Long"),

           @ApiImplicitParam(name = "limit", value = "每页数量", defaultValue = "10", paramType = "query", dataType = "Long"),

           @ApiImplicitParam(name = "startDate", value = "开始日期", paramType = "query", dataType = "String"),

           @ApiImplicitParam(name = "endDate", value = "结束日期", paramType = "query", dataType = "String"),

           @ApiImplicitParam(name = "clientIP", value = "客户端IP", paramType = "query", dataType = "String")

   })

   @GetMapping

   public Result list(

           Integer page,

           Integer limit,

           String startDate,

           String endDate,

           String clientIP

   ) {


       // 日期范围

       RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("date");


       if (StrUtil.isNotBlank(startDate)) {

           rangeQueryBuilder.from(startDate);

       }

       if (StrUtil.isNotBlank(endDate)) {

           rangeQueryBuilder.to(endDate);

       }


       BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().must(rangeQueryBuilder);


       if (StrUtil.isNotBlank(clientIP)) {

           queryBuilder.must(QueryBuilders.wildcardQuery("clientIP", "*" + clientIP + "*"));

       }

       // 总记录数

       long count = elasticSearchService.count(queryBuilder, ESConstants.LOGIN_INDEX_PATTERN);


       // 排序

       FieldSortBuilder sortBuilder = new FieldSortBuilder("@timestamp").order(SortOrder.DESC);


       // 分页查询

       List list = elasticSearchService.search(queryBuilder, sortBuilder, page, limit, LoginRecord.class, ESConstants.LOGIN_INDEX_PATTERN);


       // 遍历获取会话状态

       list.forEach(item -> {

           String token = item.getToken();

           int tokenStatus = 0;

           if (StrUtil.isNotBlank(token)) {

               tokenStatus = tokenService.getTokenStatus(item.getToken());

           }

           item.setStatus(tokenStatus);

       });


       return Result.success(list, count);

   }



   @ApiOperation(value = "删除登录记录")

   @ApiImplicitParam(name = "ids", value = "id集合", required = true, paramType = "query", dataType = "String")

   @DeleteMapping

   public Result delete(@RequestBody List documents) {

       documents.forEach(document -> elasticSearchService.deleteById(document.getId(), document.getIndex()));

       return Result.success();

   }


}


3. 强制下线接口

代码坐标: youlai-admin#TokenController


这里还是将JWT添加至黑名单,然后在网关限制被加入黑名单的JWT登录

@Api(tags = "令牌接口")

@RestController

@RequestMapping("/api.admin/v1/tokens")

@Slf4j

@AllArgsConstructor

public class TokenController {


   ITokenService tokenService;


   @ApiOperation(value = "强制下线")

   @ApiImplicitParam(name = "token", value = "访问令牌", required = true, paramType = "query", dataType = "String")

   @PostMapping("/{token}/_invalidate")

   @SneakyThrows

   public Result invalidateToken(@PathVariable String token) {

       boolean status = tokenService.invalidateToken(token);

       return Result.judge(status);

   }


}


代码坐标: youlai-admin#TokenServiceImpl


@Override

@SneakyThrows

public boolean invalidateToken(String token) {


   JWTPayload payload = JWTUtils.getJWTPayload(token);


   // 计算是否过期

   long currentTimeSeconds = System.currentTimeMillis() / 1000;

   Long exp = payload.getExp();

   if (exp < currentTimeSeconds) { // token已过期,无需加入黑名单

       return true;

   }

   // 添加至黑名单使其失效

   redisTemplate.opsForValue().set(AuthConstants.TOKEN_BLACKLIST_PREFIX + payload.getJti(), null, (exp - currentTimeSeconds), TimeUnit.SECONDS);

   return true;

}


七. 前端界面

项目前端源码:youlai-mall-admin,以下只贴出页面路径,有兴趣下载到本地查看源码和效果


代码坐标: src/views/dashboard/common/components/LoginCountChart.vue


登录次数统计、今日访问IP统计、总访问IP统计

微信图片_20230709233623.png


代码坐标: src/views/admin/record/login/index.vue


登录信息,强制用户下线,演示的是自己强制自己下线的效果

微信图片_20230709233736.gif


八. 问题

1. 日志记录登录时间比正常时间晚了8个小时

项目使用Docker部署,其中依赖openjdk镜像时区是UTC,比北京时间晚了8个小时,执行以下命令修改时区解决问题


docker exec -it youlai-auth /bin/sh

echo "Asia/Shanghai" > /etc/timezone

docker restart youlai-auth

1

2

3

微信图片_20230709233741.png


2. 用Nginx代理转发,怎么获取用户的真实IP?

在配置代理转发的时候添加:


proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

1

2

微信图片_20230709233743.png


九. Kibana索引检索

在LogStash的logout我们指定了索引的名称 “%{[project]}-%{[action]}-%{+YYYY-MM-dd}”


在logback-spring.xml指定了project为youlai-auth,action为login,替换生成类似youlai-auth-login-2021-3-25的索引,其中日期是可变的,然后我们在Kibana界面创建youlai-auth-login-*索引模式来对日志进行检索。


创建youlai-auth-login-*索引模式

微信图片_20230709233816.png

微信图片_20230709233819.png



根据索引模式,设置日期范围,进行登录日志的检索

微信图片_20230709233823.png


十. 结语

至此,整个实战过程已经完成,搭建了ELK环境,使用Spring AOP横切来对登录日志的定点的搜集,最后通过SpringBoot整合ElasticSearch的高级Java客户端RestHighLevelClient来对搜集登录日志信息进行聚合计数、统计、以及日志中访问令牌操作来实现无状态的JWT会话管理,强制JWT失效让用户下线。文中只贴出关键的代码,其中还有像IP转地区的工具使用鉴于篇幅的原因并未一一说明,完整代码请参考git上的完整源代码。点击跳转


希望大家通过本篇文章能够快速入门ElasticSearch,如果有问题欢迎留言或者加我微信(haoxianrui)。


终. 附录

欢迎大家加入开源项目有来项目交流群,一起学习Spring Cloud微服务生态组件、分布式、Docker、K8S、Vue、element-ui、uni-app、微信小程序全栈等技术。



最后附上有来项目往期文章


后台微服务


Spring Cloud实战 | 第一篇:Windows搭建Nacos服务

Spring Cloud实战 | 第二篇:Spring Cloud整合Nacos实现注册中心

Spring Cloud实战 | 第三篇:Spring Cloud整合Nacos实现配置中心

Spring Cloud实战 | 第四篇:Spring Cloud整合Gateway实现API网关

Spring Cloud实战 | 第五篇:Spring Cloud整合OpenFeign实现微服务之间的调用

Spring Cloud实战 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT实现微服务统一认证授权

Spring Cloud实战 | 最七篇:Spring Cloud Gateway+Spring Security OAuth2集成统一认证授权平台下实现注销使JWT失效方案

Spring Cloud实战 | 最八篇:Spring Cloud +Spring Security OAuth2+ Vue前后端分离模式下无感知刷新实现JWT续期

Spring Cloud实战 | 最九篇:Spring Security OAuth2认证服务器统一认证自定义异常处理

Spring Cloud实战 | 第十篇 :Spring Cloud + Nacos整合Seata 1.4.1最新版本实现微服务架构中的分布式事务,进阶之路必须要迈过的槛

Spring Cloud实战 | 第十一篇 :Spring Cloud Gateway网关实现对RESTful接口权限和按钮权限细粒度控制

后台管理前端


vue-element-admin实战 | 第一篇: 移除mock接入微服务接口,搭建SpringCloud+Vue前后端分离管理平台

vue-element-admin实战 | 第二篇: 最小改动接入后台实现根据权限动态加载菜单

微信小程序


vue+uni-app商城实战 | 第一篇:从0到1快速开发一个商城微信小程序,无缝接入Spring Cloud OAuth2认证授权登录

应用部署


Docker实战 | 第一篇:Linux 安装 Docker

Docker实战 | 第二篇:Docker部署nacos-server:1.4.0

Docker实战 | 第三篇:IDEA集成Docker插件实现一键自动打包部署微服务项目,一劳永逸的技术手段值得一试

Docker实战 | 第四篇:Docker安装Nginx,实现基于vue-element-admin框架构建的项目线上部署

Docker实战 | 第五篇:Docker启用TLS加密解决暴露2375端口引发的安全漏洞,被黑掉三台云主机的教训总结


相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
相关文章
|
7月前
|
人工智能 Kubernetes 数据可视化
Kubernetes下的分布式采集系统设计与实战:趋势监测失效引发的架构进化
本文回顾了一次关键词监测任务在容器集群中失效的全过程,分析了中转IP复用、调度节奏和异常处理等隐性风险,并提出通过解耦架构、动态IP分发和行为模拟优化采集策略,最终实现稳定高效的数据抓取与分析。
140 2
Kubernetes下的分布式采集系统设计与实战:趋势监测失效引发的架构进化
|
9月前
|
消息中间件 运维 Kafka
直播预告|Kafka+Flink双引擎实战:手把手带你搭建分布式实时分析平台!
在数字化转型中,企业亟需从海量数据中快速提取价值并转化为业务增长动力。5月15日19:00-21:00,阿里云三位技术专家将讲解Kafka与Flink的强强联合方案,帮助企业零门槛构建分布式实时分析平台。此组合广泛应用于实时风控、用户行为追踪等场景,具备高吞吐、弹性扩缩容及亚秒级响应优势。直播适合初学者、开发者和数据工程师,参与还有机会领取定制好礼!扫描海报二维码或点击链接预约直播:[https://developer.aliyun.com/live/255088](https://developer.aliyun.com/live/255088)
634 35
直播预告|Kafka+Flink双引擎实战:手把手带你搭建分布式实时分析平台!
|
5月前
|
消息中间件 Java Kafka
搭建ELK日志收集,保姆级教程
本文介绍了分布式日志采集的背景及ELK与Kafka的整合应用。传统多服务器环境下,日志查询效率低下,因此需要集中化日志管理。ELK(Elasticsearch、Logstash、Kibana)应运而生,但单独使用ELK在性能上存在瓶颈,故结合Kafka实现高效的日志采集与处理。文章还详细讲解了基于Docker Compose构建ELK+Kafka环境的方法、验证步骤,以及如何在Spring Boot项目中整合ELK+Kafka,并通过Logback配置实现日志的采集与展示。
1072 64
搭建ELK日志收集,保姆级教程
|
9月前
|
消息中间件 运维 Kafka
直播预告|Kafka+Flink 双引擎实战:手把手带你搭建分布式实时分析平台!
直播预告|Kafka+Flink 双引擎实战:手把手带你搭建分布式实时分析平台!
284 12
|
11月前
|
数据采集 存储 数据可视化
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
1213 0
分布式爬虫框架Scrapy-Redis实战指南
|
7月前
|
数据采集 缓存 NoSQL
分布式新闻数据采集系统的同步效率优化实战
本文介绍了一个针对高频新闻站点的分布式爬虫系统优化方案。通过引入异步任务机制、本地缓存池、Redis pipeline 批量写入及身份池策略,系统采集效率提升近两倍,数据同步延迟显著降低,实现了分钟级热点追踪能力,为实时舆情监控与分析提供了高效、稳定的数据支持。
322 1
分布式新闻数据采集系统的同步效率优化实战
|
11月前
|
数据可视化 关系型数据库 MySQL
ELK实现nginx、mysql、http的日志可视化实验
通过本文的步骤,你可以成功配置ELK(Elasticsearch, Logstash, Kibana)来实现nginx、mysql和http日志的可视化。通过Kibana,你可以直观地查看和分析日志数据,从而更好地监控和管理系统。希望这些步骤能帮助你在实际项目中有效地利用ELK来处理日志数据。
835 90
|
8月前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
2320 7
|
9月前
|
监控 Java 调度
SpringBoot中@Scheduled和Quartz的区别是什么?分布式定时任务框架选型实战
本文对比分析了SpringBoot中的`@Scheduled`与Quartz定时任务框架。`@Scheduled`轻量易用,适合单机简单场景,但存在多实例重复执行、无持久化等缺陷;Quartz功能强大,支持分布式调度、任务持久化、动态调整和失败重试,适用于复杂企业级需求。文章通过特性对比、代码示例及常见问题解答,帮助开发者理解两者差异,合理选择方案。记住口诀:单机简单用注解,多节点上Quartz;若是任务要可靠,持久化配置不能少。
856 4