问题
- 超卖
- 高并发
- 恶意请求
方案
数据库锁
- 行锁问题:大量锁竞争时,会影响数据库性能
update goods set inventory = inventory-1 where id = #{goodsId} and inventory > 0
- 行锁 + 乐观锁问题:库存 100,且同时只有 100 人抢购商品时,实际卖出的商品可能少于 100。同样存在性能问题。
update goods set inventory = inventory-1 where id = #{goodsId} and inventory > 0 and version = #{version}
分布式锁
- redis 分布式锁
问题:不设置锁的过期时间,可能会导致锁一致得不到释放;设置锁的过期时间,又可能因为业务执行时间较长而导致锁提前释放。使用 Lua 脚本或 Redlock 都较为复杂。 - ZooKeeper 分布式锁
问题:性能不如 Redis。
库存预热 & 内存标记
@Override
public void afterPropertiesSet() {
List<GoodsEntity> goodsList = goodsRepository.findAll();
goodsList.forEach(goods -> {
redisTemplate.opsForValue().set(SECKILL_GOODS_KEY_PREFIX + goods.getId(), String.valueOf(goods.getInventory()));
localGoodsOverMap.put(goods.getId(), false);
});
}
...
// 读取内存标记,判断商品是否售完
if (localGoodsOverMap.get(goodsId)) {
log.info("商品【{}】已售完,抢购失败!", goodsId);
return;
}
*** 分布式锁开始 ***
// 递减 redis 中库存数量,判断商品是否已售完
Long inventory = redisTemplate.opsForValue().decrement(SECKILL_GOODS_KEY_PREFIX + goodsDto.getGoodsId());
if (inventory == null || inventory < 0) {
log.info("商品【{}】已售完,抢购失败!", goodsId);
localGoodsOverMap.put(goodsId, true);
return;
}
// TODO 查库存
// TODO 减库存
// TODO 下单
*** 分布式锁结束 ***
异步下单
OrderEntity order = new OrderEntity();
order.setUserId(userId);
order.setGoodsId(goodsId);
order.setStatus(OrderStatus.TO_BE_PAID);
order.setGoodsNum(1);
// 异步下单
// 也可以异步更新库存,这就要求消费者逐个消费消息,不然也会出现并发问题
rabbitTemplate.convertAndSend(DIRECT_EXCHANGE, DIRECT_ROUTING_KEY, order);
...
@RabbitListener(queues = DIRECT_QUEUE, concurrency = "10")
public void process(OrderEntity order, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
log.info("Receive message by direct-queue: {}", order);
try {
orderRepository.save(order);
basicAck(channel, tag);
} catch (Exception e) {
log.error("创建订单失败【{}】", order);
basicNack(channel, tag);
}
/**
* 接收消息确认
*/
private void basicAck(Channel channel, long tag) {
try {
channel.basicAck(tag, false);
} catch (IOException e) {
log.error("Ack message failure", e);
}
}
/**
* 拒绝消息确认
*/
private void basicNack(Channel channel, long tag) {
try {
channel.basicNack(tag, false, false);
} catch (IOException e) {
log.error("Nack message failure", e);
}
}
按钮控制
秒杀开始之前,按钮置灰;用户抢购商品之后,按钮再次置灰。
URL 动态化
- 在秒杀之前,前端先请求后端获取商品秒杀地址。在后端生成随机数作为 pathId 存入缓存(缓存过期时间 60s),然后将这个随机数返回给前端。
- 前端获得 pathId 后,将其作为 URL 参数去请求后端秒杀服务。
- 后端接收 pathId 参数后,将其与缓存中的 pathId 比较。
示例代码如下:
public SeckillGoodsDto createSeckillUrl(String goodsId) {
String randomCode = generateRandomCode(goodsId);
redisTemplate.opsForValue().set(goodsId, randomCode);
SeckillGoodsDto goodsDto = new SeckillGoodsDto();
goodsDto.setGoodsId(goodsId);
goodsDto.setRandomCode(randomCode);
return goodsDto;
}
private String generateRandomCode(String goodsId) {
return ...;
}
public boolean buyGoods(SeckillGoodsDto goodsDto) {
// 校验商品 URL 随机码是否一致
boolean isValid = validateRandomCode(goodsDto.getGoodsId(), goodsDto.getRandomCode());
if (isValid) {
...
return true;
}
return false;
}
private boolean validateRandomCode(String goodsId, String randomCode) {
String cachedRandomCode = redisTemplate.opsForValue().get(goodsId);
return randomCode.equals(cachedRandomCode);
}
用户/IP 限流
- 前端限流
秒杀按钮在活动之前置灰,在用户购买之后再次置灰。 - 后端限流
相同用户/IP,设置请求次数限制。如可以基于 Spring Cloud Gateway 添加以下配置:
application.yml
spring:
cloud:
gateway:
routes:
# 秒杀服务
- id: seckill
uri: lb://seckill
filters:
# ip 限流
- name: RequestRateLimiter
args:
# 限流匹配策略
key-resolver: '#{@ipKeyResolver}'
# 令牌桶的填充速率:用户每秒执行多少请求
redis-rate-limiter.replenishRate: 10
# 令牌桶的容量:用户在一秒钟内执行的最大请求数
# 将此值设置为零将阻塞所有请求;将此值设置为高于 replenishRate,以允许临时突发
redis-rate-limiter.burstCapacity: 20
# 用户限流
- name: RequestRateLimiter
args:
# 限流匹配策略
key-resolver: '#{@userIdKeyResolver}'
# 令牌桶的填充速率:用户每秒执行多少请求
redis-rate-limiter.replenishRate: 10
# 令牌桶的容量:用户在一秒钟内执行的最大请求数
# 将此值设置为零将阻塞所有请求;将此值设置为高于 replenishRate,以允许临时突发
redis-rate-limiter.burstCapacity: 20
@Configuration
public class ThrottlingConfiguration {
private static final String USER_ID_NAME = "userId";
/**
* 接口限流
*/
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
/**
* ip 限流
*/
@Primary
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress());
}
/**
* 用户限流(未经身份验证直接拒绝请求)
*/
@Bean
public KeyResolver principalNameKeyResolver() {
return new PrincipalNameKeyResolver();
}
/**
* 用户限流(要求请求路径中必须携带 userId 参数)
*/
@Bean
public KeyResolver userIdKeyResolver() {
return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst(USER_ID_NAME)));
}
}
资源静态化
- JS/CSS 压缩,减少流量
- CDN 就近访问
兜底方案
- 降级
所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。 - 限流
限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。 - 拒绝服务
当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的保护方式。