系统接口优化:对接会员接口高并发和高性能优化
首先我们理一下思路,在实现高并发和高性能优化时,可以从以下几个方面着手:
1. 接口层优化
a. 限流与熔断
- 限流:通过限流工具(如 Sentinel 或 Hystrix)对关键接口设置 QPS(每秒查询数)限制,防止流量洪峰时服务崩溃。
- 熔断:配置熔断机制,当某些接口失败率达到阈值时,快速返回默认结果,避免下游系统雪崩。
b. 批量处理
- 对多次调用接口的数据进行合并处理,比如
guidePointList
和getGuideWalletBalance
的接口请求可以通过批量方式减少调用次数。
c. 缓存
- 使用 Redis 或其他缓存机制缓存高频查询的数据,例如用户信息(
getMemberInfo
)、门店数据(searchOrder
)。 - 设置合理的缓存失效时间,并根据业务场景引入 缓存预热 或 缓存更新策略。
2. 数据层优化
a. 数据库优化
-
索引:检查 SQL 中是否使用了索引,避免全表扫描。尤其是在订单查询(如
searchOrder
)中,对常用查询字段加索引。 -
分库分表:如果数据库压力较大,考虑对大表进行分库分表(如按照门店、区域或时间分表)。
-
读写分离:采用主从复制架构,将读请求分配到从库,写请求交给主库。
-
优化分页查询
:
- 使用
id > lastId LIMIT pageSize
的方式替代OFFSET
。 - 避免使用
COUNT(*)
查询总数,使用缓存或定时任务预计算。
- 使用
b. 异步化
- 对核销操作(如
checkCouponCode
和checkOrder
)通过异步处理队列(如 RocketMQ、RabbitMQ)解耦实时性和性能。
c. 结果裁剪
- 对接口响应数据精简,仅返回前端必要的字段,减少不必要的字段解析和传输。
3. 代码层优化
a. Dubbo 服务调用
- 设置超时时间:在
@Reference
注解中配置合理的超时时间(timeout
参数),避免长时间等待。 - 并发能力优化:调整 Dubbo 服务的线程池(线程数、队列大小),确保后端服务承载能力。
- 异步调用:对耗时操作采用异步调用或 CompletableFuture 提高吞吐量。
b. 日志优化
- 限制日志量:将
log.debug
和log.info
日志级别控制在生产环境可接受范围。 - 异步日志:采用 Logback 的异步日志配置,提高接口响应速度。
4. 架构层优化
a. 接口网关
- 引入网关(如 Spring Cloud Gateway 或 NGINX),进行统一的接口限流、缓存、路由转发,提升系统的抗压能力。
b. 分布式事务优化
- 避免全局事务:
- 使用本地事务+补偿机制(TCC)。
- 对非核心业务(如积分推送
pushTrainIntegral
),采用最终一致性策略。
c. 分布式锁优化
- 使用 Redis 分布式锁时,设置合理的过期时间,避免死锁。
- 如果性能压力大,可以尝试使用 Redisson 提供的高效锁实现。
5. 性能监控与压测
a. 性能监控
- 监控接口性能指标(响应时间、吞吐量、错误率),例如:
- 使用 Prometheus + Grafana 实现系统指标的实时监控。
- 使用 Zipkin 或 Skywalking 跟踪分布式调用链。
b. 压力测试
- 使用 JMeter 或 Locust 对接口进行模拟压测,定位系统瓶颈,优化慢接口。
6. 示例优化
示例:接口限流与缓存
@PostMapping(value = "/scrm/scrm-coupon-code-check-valid")
public BaseResponse<ValidCouponResp> checkValidCouponCode(@RequestBody ValidCouponRequest validCouponRequest) {
// 尝试从缓存中获取结果
String cacheKey = "coupon:check:" + validCouponRequest.getCouponCode();
ValidCouponResp cachedResp = redisTemplate.opsForValue().get(cacheKey);
if (cachedResp != null) {
return BaseResponse.success(cachedResp);
}
// 限流
if (!rateLimiter.tryAcquire()) {
return BaseResponse.fail("系统繁忙,请稍后重试");
}
// 调用下游服务
BaseResponse<ValidCouponResp> response = scrmFacade.checkValidCouponCode(validCouponRequest);
// 缓存结果
if (response.isSuccess()) {
redisTemplate.opsForValue().set(cacheKey, response.getData(), 10, TimeUnit.MINUTES);
}
return response;
}
通过以上多维度的优化,可以显著提升系统的并发处理能力和整体性能。你可以根据实际业务场景,选择适合的优化策略逐步落地实施。
我的实际解决方案
原方法
1.Controller层,没有做熔断限流等策略
@PostMapping(value = "/RESTAdapter/retail/retail-order-count")
public IntegrateCommonResp findRetailOrderCount(@RequestBody RetailOrderReqVM retailOrderReqVM)
{
if(StringUtils.isEmpty(retailOrderReqVM.getFrameNumber())&&StringUtils.isEmpty(retailOrderReqVM.getPurchaserMobile())){
throw new ApplicationException(String.format("参数为空:%s",retailOrderReqVM.toString()));
}
BaseResponse<Long> response =retailOrderFacade.findConcurRetailOrderCount(retailOrderReqVM);
return transfer(response);
}
2.然后给这个调用的方法,加一个缓存
public Long findConcurRetailOrderCount(RetailOrderReqVM retailOrderReqVM) {
Long count = retailOrderService.findRetailOrderCount(retailOrderReqVM);
if (count > 0) {
BaseSysParamSearchDTO req = new BaseSysParamSearchDTO();
req.setParamCode("ORDER_NUM_LIMIT");
req.setSetsOfBooksId(Constants.DEFAULT_SETS_OF_BOOKS_ID);
BaseResponse<BaseSysParamResponseDTO> paramResp = baseSysParamFacade.getConcurrentSysParam(req);
if (paramResp.isSuccess() && paramResp.getContent() != null) {
if (count > Integer.valueOf(paramResp.getContent().getParamValue())) {
throw new ApplicationException(String.format("一个手机号超过%s张订单数限制", paramResp.getContent().getParamValue()));
}
}
}
return count;
}
3.然后调用系统参数的时候,改为CAS自旋
/**
* 获取系统参数
*/
public BaseSysParamResponseDTO getSysParam(BaseSysParamSearchDTO request) {
BaseSysParam querySystemParam = new BaseSysParam();
mapper(request, querySystemParam);
BaseSysParam sp = getParamValueFirstCache(querySystemParam);
if (Objects.nonNull(sp)) {
return sp.convertTo(BaseSysParamResponseDTO.class);
}
// 当setsOfBooksId !=null,使用参数查询不到,修改成-1再次查询<br>
querySystemParam.setSetsOfBooksId(Constants.DEFAULT_SETS_OF_BOOKS_ID);
querySystemParam.setTenantId(Constants.DEFAULT_TENANT_ID);
BaseSysParam sp1 = this.getOne(new QueryWrapper<>(querySystemParam));
if (Objects.nonNull(sp1)) {
RedisLockUtil.executeSynchOperate(locked -> {
if (locked) {
// 复制一份,修改账套为传递进来的账套id和租户id
sp1.setSetsOfBooksId(request.getSetsOfBooksId());
sp1.setIsGlobalUse(TrueOrFalseEnum.FALSE.getValue());
super.save(sp1);
saveCache(sp1);
return 1L;
} else {
log.error("获取账套系统参数,竞争锁失败,请稍后刷新数据后重试");
throw new ApplicationException(CommonErrorCode.LOCK_FAIL);
}
}, StringUtil.connectStr(BaseCenterConstants.LOCK_KEY_PREFIX_SYSTEM_PARAM, request.getSetsOfBooksId() + "", querySystemParam.getParamCode()), 5000);
} else {
throw new ApplicationException(BaseCenterErrorCode.SYSTEM_PARAM_NOT_FIND.getCode(),
String.format(BaseCenterErrorCode.SYSTEM_PARAM_NOT_FIND.getMessage(), request.getParamCode()));
}
return sp1.convertTo(BaseSysParamResponseDTO.class);
}
解决方案:
索引查了没什么好改的,代码上也没什么,也没事务,就下面两个还可以大优化一下:
1. 增加熔断限流策略:限制外接接口对本系统负载
为外接接口增加熔断限流,保护本系统资源,避免因外部接口过载导致系统性能问题。推荐使用 Resilience4j(如上文所述)。它提供了 RateLimiter
和 CircuitBreaker
等功能,集成简单,性能优秀。
- 添加依赖
在 pom.xml
中添加 Resilience4j 的依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.0.2</version>
</dependency>
- 配置 Resilience4j
配置思路:
- 熔断器配置
根据每个服务实例的处理能力调整熔断参数,重点关注失败率、滑动窗口大小等:failure-rate-threshold
: 默认值 50%,根据实际外接接口可靠性可调整到 40%-60%。sliding-window-size
: 如果每秒有大量请求,可以使用更大的窗口(如 10-20)。minimum-number-of-calls
: 避免因低调用量导致误触发熔断器,可设置为略大于每秒最小预期调用量(如 5-10 次)。wait-duration-in-open-state
: 熔断后恢复间隔要足够长以避免短时间内频繁触发(如 20s)。
- 限流器配置
需要综合考虑系统的并发能力和外接接口的吞吐性能:limit-for-period
: 每秒最大允许请求量应接近容器实际的处理能力。 如果容器有 4 个实例,每个实例理论支持 500 QPS,总量为 2000 QPS,可设置为单实例约 400-500。limit-refresh-period
: 根据业务特性选择刷新频率,通常 1 秒。timeout-duration
: 限流时最大等待时间,建议不要过高(如 300-500ms)。
在 integrate-impl.yml
中添加熔断器和限流器的配置:
resilience4j:
circuitbreaker:
instances:
externalApi:
register-health-indicator: true
failure-rate-threshold: 50 # 失败率超过 50% 触发熔断
wait-duration-in-open-state: 20s # 熔断后等待 20 秒尝试恢复
sliding-window-size: 10 # 滑动窗口大小,记录最近 10 次调用
sliding-window-type: COUNT_BASED # 基于调用计数的窗口模式
minimum-number-of-calls: 5 # 最少触发熔断的调用次数
ratelimiter:
instances:
externalApi:
limit-for-period: 400 # 每秒最多允许 400 次请求
limit-refresh-period: 1s # 刷新限流窗口周期为 1 秒
timeout-duration: 300ms # 等待获取许可的最大时间为 300 毫秒
- 编写接口调用逻辑
定义一个服务类,用于调用外部接口并集成 Resilience4j 的功能。
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Slf4j
@Service
public class ExternalApiService {
private final RestTemplate restTemplate = new RestTemplate();
// 配置熔断器和限流器,实例名称为 externalApi
@CircuitBreaker(name = "externalApi", fallbackMethod = "fallbackResponse")
@RateLimiter(name = "externalApi")
public String callExternalApi(String url) {
log.info("调用外部接口: {}", url);
return restTemplate.getForObject(url, String.class);
}
// 熔断触发时的回退方法
public String fallbackResponse(String url, Throwable throwable) {
log.error("外部接口调用失败,触发熔断或限流。URL: {}, 错误: {}", url, throwable.getMessage());
return "服务不可用,请稍后再试。";
}
}
- 控制器层调用服务
编写一个 Controller 调用 ExternalApiService
中的方法:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ExternalApiController {
private final ExternalApiService externalApiService;
public ExternalApiController(ExternalApiService externalApiService) {
this.externalApiService = externalApiService;
}
@GetMapping("/call-api")
public String callExternalApi(@RequestParam String url) {
return externalApiService.callExternalApi(url);
}
}
代码说明
- 熔断器配置
failure-rate-threshold
: 当调用失败率达到 50% 时熔断器触发。wait-duration-in-open-state
: 熔断器打开后等待 10 秒再尝试恢复。sliding-window-type
: 使用计数窗口,每个窗口记录最近 5 次调用。
- 限流器配置
limit-for-period
: 每秒最多允许 10 个请求。limit-refresh-period
: 每 1 秒刷新限流窗口。timeout-duration
: 如果没有立即获取许可,最多等待 500 毫秒。
- 熔断回退方法
- 在调用失败或限流触发时,
fallbackResponse
方法将被执行,返回友好的降级提示。
- 在调用失败或限流触发时,
- 监控和管理
Resilience4j 集成了 Actuator,可以通过/actuator
端点查看熔断器和限流器的状态。
2. 缓存已经有了,可以优化 Redis 分布式锁自旋
针对 Redis 分布式锁的实现,优化点包括:
- 减少锁竞争冲突:通过加锁后再检查缓存避免重复操作。
- 实现退避算法:加入指数退避减少频繁锁竞争。
- 可配置化锁重试和等待时间:提升代码灵活性。
改成下面的:
public BaseSysParamResponseDTO getConcurrentSysParam(BaseSysParamSearchDTO request) {
BaseSysParam querySystemParam = new BaseSysParam();
mapper(request, querySystemParam);
// 第一次检查缓存
BaseSysParam sp = getParamValueFirstCache(querySystemParam);
if (Objects.nonNull(sp)) {
return sp.convertTo(BaseSysParamResponseDTO.class);
}
// 当 setsOfBooksId 不为 null 时,使用参数查询不到,修改成 -1 再次查询
querySystemParam.setSetsOfBooksId(Constants.DEFAULT_SETS_OF_BOOKS_ID);
querySystemParam.setTenantId(Constants.DEFAULT_TENANT_ID);
BaseSysParam sp1 = this.getOne(new QueryWrapper<>(querySystemParam));
if (Objects.nonNull(sp1)) {
boolean lockAcquired = false;
int retryCount = 5; // 重试次数
int baseWaitTime = 100; // 基础等待时间 100 毫秒
while (retryCount-- > 0 && !lockAcquired) {
lockAcquired = RedisLockUtil.executeSynchOperate(locked -> {
if (locked) {
// 第二次检查缓存
BaseSysParam spCheck = getParamValueFirstCache(querySystemParam);
if (Objects.nonNull(spCheck)) {
return true; // 缓存命中,不需要再保存新的参数
}
sp1.setSetsOfBooksId(request.getSetsOfBooksId());
sp1.setTenantId(request.getTenantId());
sp1.setCreationDate(LocalDateTime.now());
sp1.setLastUpdateDate(LocalDateTime.now());
sp1.setCreatedBy(request.getCurrUserLoginId());
sp1.setLastUpdatedBy(request.getCurrUserLoginId());
sp1.setIsGlobalUse(TrueOrFalseEnum.FALSE.getValue());
super.save(sp1);
saveCache(sp1);
return true;
} else {
log.error("获取账套系统参数,竞争锁失败,请稍后刷新数据后重试");
return false;
}
}, StringUtil.connectStr(BaseCenterConstants.LOCK_KEY_PREFIX_SYSTEM_PARAM,
request.getSetsOfBooksId() + "",
querySystemParam.getParamCode()),
30000);
if (!lockAcquired) {
// 短时间自旋等待,减少CPU占用
// int spinWaitTime = (int) (baseWaitTime * Math.pow(2, 5 - retryCount)); // 指数退避算法
// long end = System.currentTimeMillis() + spinWaitTime;
// while (System.currentTimeMillis() < end) {
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// throw new ApplicationException(CommonErrorCode.LOCK_FAIL);
// }
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ApplicationException(CommonErrorCode.LOCK_FAIL);
}
}
}
if (!lockAcquired) {
throw new ApplicationException(CommonErrorCode.LOCK_FAIL);
}
} else {
throw new ApplicationException(BaseCenterErrorCode.SYSTEM_PARAM_NOT_FIND.getCode(),
String.format(BaseCenterErrorCode.SYSTEM_PARAM_NOT_FIND.getMessage(), request.getParamCode()));
}
// 返回参数数据
return sp1.convertTo(BaseSysParamResponseDTO.class);
}