Spring Boot3单应用并发抢券逻辑:从问题到完美解决
作为互联网软件开发人员,咱们在做活动营销模块时,肯定绕不开并发抢券这个坎吧?是不是经常遇到用户抢券时出现超卖,明明库存只有 100 张,结果最后发出去 120 张的情况?还有就是并发量一上来,系统就卡得不行,用户抱怨连连,领导也跟着着急。这些问题要是解决不好,不仅影响用户体验,严重的还可能给公司带来经济损失和声誉影响。
单应用架构下并发抢券的难点
单应用架构不像分布式架构那样可以通过多节点分担压力,所有的请求都集中在一个应用实例上,这就对咱们的代码逻辑和资源调度提出了更高的要求。而且抢券场景有个特点,就是在某个时间点会有大量用户同时涌入,比如整点抢券的时候,瞬间的高并发很容易让系统 “罢工”。同时,库存的准确性是核心,一旦出现超卖,后续的退款、用户投诉等一系列问题都会接踵而至,处理起来特别麻烦。所以,在单应用架构下实现一套稳定、高效的并发抢券逻辑,是咱们开发过程中必须攻克的难题。
Spring Boot3 并发抢券逻辑实现方案
接下来,就给大家详细讲讲在 Spring Boot3 中如何实现这套并发抢券逻辑,全是干货,代码都给大家准备好了,可以直接参考着用。
(1)用乐观锁解决超卖问题
先解决超卖这个核心问题,这里咱们可以用乐观锁机制。在数据库的优惠券库存表中加一个 version 字段,每次更新库存的时候都检查版本号。
实体类 Coupon 中添加 version 字段:
@Entity
@Table(name = "coupon")
public class Coupon {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer stock;
@Version
private Integer version;
// getter和setter方法省略
}
Service 层的扣减库存方法:
@Service
public class CouponService {
@Autowired
private CouponRepository couponRepository;
@Transactional
public boolean deductStock(Long couponId) {
Coupon coupon = couponRepository.findById(couponId).orElse(null);
if (coupon == null || coupon.getStock() <= 0) {
return false;
}
coupon.setStock(coupon.getStock() - 1);
try {
couponRepository.save(coupon);
return true;
} catch (ObjectOptimisticLockingFailureException e) {
// 乐观锁冲突,说明有其他线程已经修改了数据
return false;
}
}
}
这样一来,当多个线程同时抢券时,只有一个线程能成功扣减库存,有效避免了超卖。
(2)Redis 缓存减轻数据库压力
为了提高系统的并发处理能力,咱们可以用 Redis 做缓存,减少对数据库的访问压力。在项目启动时,把优惠券的库存信息加载到 Redis 中。
配置 Redis:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
项目启动时加载库存到 Redis:
@Component
public class CouponStockInitializer implements CommandLineRunner {
@Autowired
private CouponRepository couponRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void run(String... args) throws Exception {
List<Coupon> coupons = couponRepository.findAll();
for (Coupon coupon : coupons) {
redisTemplate.opsForValue().set("coupon:stock:" + coupon.getId(), coupon.getStock());
}
}
}
抢券时先操作 Redis 再异步更新数据库:
@Service
public class GrabCouponService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private CouponService couponService;
@Autowired
private AsyncTaskService asyncTaskService;
public boolean grabCoupon(Long couponId, Long userId) {
// 先检查用户是否已经抢过券,避免重复抢券
Boolean isGrabbed = redisTemplate.opsForSet().isMember("coupon:grabbed:" + couponId, userId);
if (Boolean.TRUE.equals(isGrabbed)) {
return false;
}
// 从Redis中扣减库存
Long stock = redisTemplate.opsForValue().decrement("coupon:stock:" + couponId);
if (stock != null && stock >= 0) {
// 记录用户抢券信息
redisTemplate.opsForSet().add("coupon:grabbed:" + couponId, userId);
// 异步更新数据库
asyncTaskService.updateCouponStock(couponId);
return true;
} else {
// 库存不足,把刚才扣减的加回来
redisTemplate.opsForValue().increment("coupon:stock:" + couponId);
return false;
}
}
}
(3)启用虚拟线程提升吞吐量
咱们可以利用 Spring Boot3 的虚拟线程来提升系统吞吐量,只需要在配置文件中添加:
spring:
threads:
virtual:
enabled: true
然后在 Controller 层的接口上使用 @Async 注解,配合虚拟线程池:
@RestController
@RequestMapping("/coupon")
public class CouponController {
@Autowired
private GrabCouponService grabCouponService;
@PostMapping("/grab")
public ResponseEntity<String> grabCoupon(@RequestParam Long couponId, @RequestParam Long userId) {
boolean success = grabCouponService.grabCoupon(couponId, userId);
if (success) {
return ResponseEntity.ok("抢券成功");
} else {
return ResponseEntity.ok("抢券失败,库存不足或已抢过");
}
}
}
(4)配置 Tomcat 线程池
合理配置 Tomcat 线程池也很重要,在配置文件中这样设置:
server:
tomcat:
max-threads: 200
min-spare-threads: 50
(5)异步处理耗时任务
对于一些耗时的操作,比如记录抢券日志、发送通知等,咱们可以用 @Async 注解异步处理:
@Service
public class AsyncTaskService {
@Autowired
private CouponRepository couponRepository;
@Autowired
private CouponLogRepository couponLogRepository;
@Async
public void updateCouponStock(Long couponId) {
// 异步更新数据库库存
couponRepository.deductStock(couponId);
// 记录抢券日志
CouponLog log = new CouponLog();
log.setCouponId(couponId);
log.setCreateTime(new Date());
couponLogRepository.save(log);
// 发送通知等其他耗时操作
}
}
总结
总结一下,在 Spring Boot3 中实现单应用架构下的并发抢券逻辑,核心就是用乐观锁防止超卖,用 Redis 缓存减轻数据库压力,启用虚拟线程和合理配置线程池提升并发能力,再结合异步处理耗时任务。这套方案亲测有效,大家可以放心拿去用。
要是在实际应用中遇到什么问题,或者有更好的优化思路,欢迎在评论区分享出来,咱们一起交流学习,让咱们的代码更健壮,系统更稳定!