Spring Boot3 单实例应用中高并发抢券的实现与优化

4 阅读8分钟

前言

在当今互联网软件开发的浪潮中,电商促销、限时抢购等活动屡见不鲜,而抢券功能作为其中的关键环节,如何在高并发场景下保证其高效、稳定运行,成为了广大互联网软件开发人员关注的焦点。尤其在单实例应用中,受限于资源和架构,实现高并发抢券面临着诸多挑战。今天,咱们就来深入探讨一下在 Spring Boot3 框架下,如何实现一套强大的单实例应用高并发抢券逻辑。

高并发抢券系统通常涉及到大量用户在极短时间内同时对优惠券进行争抢,这带来了诸多挑战。首先,库存有限,必须在短时间内处理大量用户请求,同时要确保库存数据的准确性,避免超卖现象的发生。其次,系统需要具备高可用性,在高并发场景下依然能够稳定运行,不能出现崩溃或宕机的情况。再者,数据一致性至关重要,由于涉及到库存等关键数据,在高并发下必须保证数据的一致性,否则会引发一系列问题。

优化数据库连接池

数据库连接池的大小对于应用的性能有着直接的影响。合理配置数据库连接池可以大幅提升并发处理能力。在 Spring Boot3 中,默认使用的是 HikariCP 连接池,我们可以通过修改配置文件来调整其参数。例如在application.properties中进行如下配置:

#配置HikariCP为SpringBoot的数据库连接池
spring.datasource.hikari.maximum-pool-size=20
#设置连接池的最大大小,一个经验法则是(CPU核心数 * 2) + 1,因为虚拟线程哲学是“快速借用,快速归还”,小连接池配合快速SQL执行也能服务大量请求
spring.datasource.hikari.minimum-idle=10
#设置连接池的最小空闲连接,可和最大值设成一样,避免高峰期动态创建连接的开销
spring.datasource.hikari.connection-timeout=2000
#连接超时时间,如果连接池满了,一个请求最多等2秒,快速失败
spring.datasource.hikari.max-lifetime=600000
#最大生命周期,一个连接在池里活10分钟,避免因网络问题产生死连接

通过这样的配置,我们能够让数据库连接池更加高效地工作,在高并发抢券场景下,为应用提供稳定的数据库连接支持,避免因连接池耗尽或连接等待时间过长导致的性能问题。

使用缓存减少数据库访问

频繁的数据库访问会显著降低应用的响应速度,尤其在高并发抢券时,对数据库的压力会呈指数级增长。通过使用缓存,我们可以减少数据库的访问次数,从而提升并发能力。在 Spring Boot3 项目中,我们可以使用 Redis 作为缓存工具。

在项目启动时,将优惠券的初始库存信息加载到 Redis 缓存中。在抢券逻辑中,先从缓存中获取库存进行判断和更新。示例代码如下:

@Autowired
private RedisTemplate<String, Integer> redisTemplate;

public String grabCoupon(String couponId) {
    Integer stock = redisTemplate.opsForValue().get("coupon:stock:" + couponId);
    if (stock != null && stock > 0) {
        redisTemplate.opsForValue().decrement("coupon:stock:" + couponId);
        // 这里可以添加更新数据库库存的逻辑,为保证数据一致性,可采用异步方式更新
        return "抢券成功";
    } else {
        return "优惠券已抢光";
    }
}

通过这种方式,大部分的抢券请求可以在缓存层快速处理,只有在缓存更新后需要同步数据库时才会访问数据库,大大减轻了数据库的压力,提升了系统的并发处理能力。同时,要注意缓存和数据库数据的一致性问题。可以采用异步更新数据库的方式,在缓存更新成功后,发送一个消息到消息队列,由消费者异步更新数据库,并且在更新数据库失败时,要有相应的补偿机制,比如重试或者回滚缓存操作。

异步处理耗时任务

在抢券过程中,可能会有一些耗时任务,如记录用户抢券日志、发送抢券成功通知等。如果这些任务同步执行,会阻塞主线程,影响抢券的并发性能。我们可以利用 Spring 的@Async注解将这些耗时任务异步化。

首先,在配置类中开启异步处理功能:

@Configuration
@EnableAsync
public class AsyncConfig {
}

然后,在需要异步执行的方法上添加@Async注解:

@Service
public class CouponService {
    @Async
    public void recordCouponLog(String userId, String couponId) {
        // 记录用户抢券日志的逻辑
    }

    @Async
    public void sendCouponNotification(String userId, String couponId) {
        // 发送抢券成功通知的逻辑
    }
}

这样,在用户抢券时,主线程可以快速返回抢券结果,而将耗时的日志记录和通知发送任务交给异步线程去处理,提高了系统的响应速度和并发处理能力。

合理配置 Tomcat 线程池

Spring Boot3 内置的 Tomcat 服务器的线程池配置也会影响并发能力。适当增加线程池的大小可以处理更多的并发请求,但也不能无限制增加,否则会导致资源耗尽。我们可以在application.properties中对 Tomcat 线程池进行配置:

server.tomcat.max-threads=200
#设置Tomcat线程池的最大线程数,可根据服务器硬件资源和实际业务场景进行调整
server.tomcat.min-spare-threads=20
#设置Tomcat线程池的最小备用线程数,保证有一定数量的线程随时可用

通过合理调整 Tomcat 线程池参数,能够让 Tomcat 服务器在高并发抢券场景下更好地处理请求,避免因线程不足导致请求积压,提升系统的整体并发性能。

启用虚拟线程

Java 19 带来的 Project Loom(并在 Java 21 成为正式功能)中的虚拟线程,是一种由 JVM 自己管理的超轻量级线程。成千上万个虚拟线程可以被映射到一小组 OS 平台线程上运行。当虚拟线程遇到 I/O 阻塞时,它不会霸占平台线程,而是会被 “卸载”,让平台线程去执行其他任务,这简直就是为 I/O 密集型应用量身定做的,而抢券场景通常就是 I/O 密集型的。

在 Spring Boot 3 里启用虚拟线程,简单到令人发指,只需要在application.properties里加一行配置:

# 就这一行,你的Tomcat就开始用虚拟线程处理请求了
spring.threads.virtual.enabled=true 

开启虚拟线程后,Tomcat 处理请求的模型会发生变化,能够以极少的平台线程处理大量的请求,服务器的吞吐量瞬间就上去了,大大提升了系统在高并发抢券场景下的处理能力。不过启用虚拟线程后,相关的一些配置也需要调整,比如数据库连接池的大小,就不需要设置得过大,前文提到的根据经验法则设置为 (CPU 核心数 * 2) + 1 就是考虑到虚拟线程的特性。

限流与降级

在高并发场景下,限流和降级是保护系统的重要手段,可以防止系统因过载而崩溃。我们可以使用一些限流算法,如令牌桶算法或漏桶算法来实现限流。以 Google Guava 的 RateLimiter 为例,使用它进行限流的代码如下:

import com.google.common.util.concurrent.RateLimiter;
@Service
public class RateLimitService {
    private RateLimiter rateLimiter = RateLimiter.create(10.0); 
    //每秒不超过10个任务,可根据实际业务场景调整
    public void rateLimitedMethod() {
        if (rateLimiter.tryAcquire()) {
            //执行抢券任务
        } else {
            //达到限流条件,进行降级处理,比如返回提示信息告知用户稍后再试
        }
    }
}

同时,在系统出现异常或资源不足时,需要进行降级处理。例如,当数据库连接池耗尽时,可以暂时关闭一些非关键的功能,如抢券成功后的积分赠送功能,优先保证核心的抢券功能能够正常运行。

数据库优化

在高并发抢券场景下,数据库的优化至关重要。除了前面提到的合理配置数据库连接池,还可以采用数据库的乐观锁机制,在更新库存等数据时进行版本号的比较和更新,防止数据冲突。

在优惠券实体类中添加版本号字段:

@Entity
@Table(name = "coupons")
public class Coupon {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Integer stock;
    @Version
    private Integer version;
    // 其他字段和方法省略
}

在更新库存的方法中使用乐观锁,通过@Transactional注解确保整个更新操作的原子性:

@Service
@Transactional
public class CouponService {
    @Autowired
    private CouponRepository couponRepository;
    public String updateCouponStock(String couponId) {
        Coupon coupon = couponRepository.findById(couponId).orElse(null);
        if (coupon != null && coupon.getStock() > 0) {
            coupon.setStock(coupon.getStock() - 1);
            couponRepository.save(coupon);
            return "抢券成功";
        } else {
            return "优惠券已抢光";
        }
    }
}

这样,在并发更新库存时,如果有其他事务已经修改了该优惠券的版本号,当前事务会因为版本号不一致而更新失败,从而避免了数据冲突和超卖现象。

通过以上多种方法的综合运用,我们能够在 Spring Boot3 单实例应用中实现高并发抢券功能,并保证系统的高效、稳定运行,为用户提供流畅的抢券体验。当然,在实际应用中,还需要根据具体的业务场景和服务器资源情况进行进一步的调整和优化。