引言
在电商平台的秒杀运动中,高并发场景下的抢购超卖Bug是一个常见且棘手的问题。一旦处理不妥,不仅会引发用户投诉,还会对商家的信誉和长处造成严重损害。本文将具体先容秒杀抢购超卖Bug的背景汗青、业务场景、底层原理以及Java代码实现,旨在资助开辟者更好地理解和解决这一问题。
背景汗青
秒杀运动的劈头与发展
秒杀运动劈头于电商平台的促销运动,旨在通过限时限量的低价商品吸引大量用户参与,从而提拔平台流量和销售额。随着电商行业的快速发展,秒杀运动逐渐成为各大电商平台的标配,每年双11、618等大型购物节期间,秒杀运动更是成为用户抢购的热点。
超卖问题的出现与影响
然而,在高并发场景下,秒杀运动往往面对超卖问题的挑战。所谓超卖,是指在商品库存已经售罄的环境下,系统仍然允许用户下单购买,导致实际售出的商品数量凌驾库存数量。超卖问题不仅会导致商家亏损,还会引发用户投诉和信任危急,对平台的声誉和恒久发展造成严重影响。
业务场景
秒杀运动的典范特点
秒杀运动具有以下几个典范特点:
- 定时触发,流量瞬间突增:秒杀运动通常在固定时间点开始,用户会在运动开始前大量涌入,导致系统流量瞬间突增。
- 哀求量远大于库存量:由于秒杀商品的代价远低于市场价,因此会吸引大量用户参与抢购,导致哀求量远大于库存量。
- 业务逻辑简朴,但并发要求高:秒杀运动的业务逻辑相对简朴,重要是库存扣减和订单天生,但对系统的并发处理本事要求极高。
秒杀流程概述
秒杀流程通常包括以下几个步调:
- 用户哀求秒杀:用户在秒杀运动开始后,通过前端页面发起秒杀哀求。
- 系统校验哀求:系统对用户的秒杀哀求进行校验,包括用户身份验证、商品库存检查等。
- 库存扣减:如果校验通过,系统执行库存扣减操作,将商品库存数量减一。
- 天生订单:库存扣减乐成后,系统天生秒杀订单,并将订单信息存储到数据库中。
- 返回结果给用户:系统将秒杀结果返回给用户,包括秒杀乐成或失败的信息。
底层原理
数据库层面的并发控制
在高并发场景下,数据库层面的并发控制是防止超卖问题的关键。常见的并发控制本事包括悲观锁、乐观锁和分布式锁等。
悲观锁
悲观锁是一种独占锁,它在数据读取时就会对数据进行加锁,以防止其他事务对数据进行修改。在秒杀场景中,可以利用数据库的排他锁(如MySQL的SELECT ... FOR UPDATE)来实现悲观锁。然而,悲观锁会导致其他事务在读取数据时被阻塞,从而低落系统的并发处理本事。
乐观锁
乐观锁是一种乐观的并发控制本事,它假设在数据读取到提交更新的这段时间内,数据不会被其他事务修改。在更新数据时,乐观锁会检查数据是否被其他事务修改过,如果被修改过,则放弃更新操作。在秒杀场景中,可以利用数据库的版本号(如MySQL的version字段)来实现乐观锁。然而,乐观锁在高并发场景下可能存在更新乐成率较低的问题。
分布式锁
分布式锁是一种跨进程的锁机制,它可以在分布式系统中实现多个进程之间的互斥。在秒杀场景中,可以利用Redis等分布式缓存系统来实现分布式锁。然而,分布式锁的实现须要考虑锁的失效问题、死锁问题等。
缓存层面的优化
为了减轻数据库的压力,进步系统的并发处理本事,可以在缓存层面进行优化。常见的缓存优化本事包括利用Redis等内存数据库来缓存库存数据、利用消息队列来削峰填谷等。
Redis缓存库存数据
在秒杀运动开始前,可以将商品库存数据加载到Redis缓存中。在秒杀过程中,系统可以直接从Redis缓存中读取库存数据,并执行库存扣减操作。这样可以制止直接访问数据库,从而减轻数据库的压力。
消息队列削峰填谷
在秒杀运动开始前,可以将用户的秒杀哀求写入消息队列中。后端服务可以按照顺序从消息队列中消费秒杀哀求,并执行库存扣减和订单天生操作。这样可以将高并发的秒杀哀求分散到差别的时间段内处理,从而制止系统瞬间过载。
Java代码实现
环境准备
在进行Java代码实现之前,须要准备以下环境:
- JDK:Java开辟工具包,用于编译和运行Java代码。
- Maven:项目构建工具,用于管理项目的依赖和构建过程。
- Redis:分布式缓存系统,用于缓存库存数据和实现分布式锁。
- MySQL:关系型数据库管理系统,用于存储商品和订单数据。
项目布局
项目布局如下:
- 复制代码
- seckill-demo
- ├── src
- │ ├── main
- │ │ ├── java
- │ │ │ └── com
- │ │ │ └── example
- │ │ │ └── seckill
- │ │ │ ├── SeckillApplication.java
- │ │ │ ├── controller
- │ │ │ │ └── SeckillController.java
- │ │ │ ├── service
- │ │ │ │ ├── SeckillService.java
- │ │ │ │ └── impl
- │ │ │ │ └── SeckillServiceImpl.java
- │ │ │ ├── repository
- │ │ │ │ └── SeckillRepository.java
- │ │ │ └── util
- │ │ │ └── RedisLockUtil.java
- │ │ └── resources
- │ │ ├── application.properties
- │ │ └── mybatis-config.xml
- └── pom.xml
复制代码 依赖配置
在pom.xml文件中添加项目所需的依赖:
- <dependencies>
- <!-- Spring Boot Starter Web -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <!-- Spring Boot Starter Data JPA -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
- <!-- MySQL Connector -->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <!-- Redis Client -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
- <!-- MyBatis Spring Boot Starter -->
- <dependency>
- <groupId>org.mybatis.spring.boot</groupId>
- <artifactId>mybatis-spring-boot-starter</artifactId>
- <version>2.2.0</version>
- </dependency>
- <!-- Lombok -->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- </dependencies>
复制代码 数据库配置
在application.properties文件中配置数据库连接信息:
- spring.datasource.url=jdbc:mysql://localhost:3306/seckill_demo?useSSL=false&serverTimezone=UTC
- spring.datasource.username=root
- spring.datasource.password=root
- spring.jpa.hibernate.ddl-auto=update
- spring.jpa.show-sql=true
- spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
复制代码 实体类
创建商品和订单实体类:
- package com.example.seckill.entity;
- import lombok.Data;
- import javax.persistence.Entity;
- import javax.persistence.GeneratedValue;
- import javax.persistence.GenerationType;
- import javax.persistence.Id;
- @Data
- @Entity
- public class SeckillGoods {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
- private String name;
- private Integer stock;
- private Integer price;
- }
- @Data
- @Entity
- public class SeckillOrder {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
- private Long userId;
- private Long goodsId;
- private Integer orderPrice;
- private Integer orderStatus;
- }
复制代码 Repository接口
创建商品和订单的Repository接口:
- package com.example.seckill.repository;
- import com.example.seckill.entity.SeckillGoods;
- import com.example.seckill.entity.SeckillOrder;
- import org.springframework.data.jpa.repository.JpaRepository;
- import org.springframework.stereotype.Repository;
- @Repository
- public interface SeckillGoodsRepository extends JpaRepository<SeckillGoods, Long> {
- }
- @Repository
- public interface SeckillOrderRepository extends JpaRepository<SeckillOrder, Long> {
- }
复制代码 Service层
创建秒杀服务接口和实现类:
- package com.example.seckill.service;
- import com.example.seckill.entity.SeckillGoods;
- import com.example.seckill.entity.SeckillOrder;
- public interface SeckillService {
- SeckillGoods getGoodsById(Long goodsId);
- boolean reduceStock(Long goodsId);
- SeckillOrder createOrder(Long userId, Long goodsId);
- }
- package com.example.seckill.service.impl;
- import com.example.seckill.entity.SeckillGoods;
- import com.example.seckill.entity.SeckillOrder;
- import com.example.seckill.repository.SeckillGoodsRepository;
- import com.example.seckill.repository.SeckillOrderRepository;
- import com.example.seckill.service.SeckillService;
- import com.example.seckill.util.RedisLockUtil;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- @Service
- public class SeckillServiceImpl implements SeckillService {
- @Autowired
- private SeckillGoodsRepository goodsRepository;
- @Autowired
- private SeckillOrderRepository orderRepository;
- @Autowired
- private StringRedisTemplate redisTemplate;
- @Override
- public SeckillGoods getGoodsById(Long goodsId) {
- return goodsRepository.findById(goodsId).orElse(null);
- }
- @Override
- @Transactional
- public boolean reduceStock(Long goodsId) {
- // 使用Redis分布式锁防止超卖
- String lockKey = "seckill_lock_" + goodsId;
- boolean lockAcquired = RedisLockUtil.tryLock(redisTemplate, lockKey, 10);
- if (!lockAcquired) {
- return false; // 获取锁失败,返回秒杀失败
- }
- try {
- SeckillGoods goods = goodsRepository.findById(goodsId).orElse(null);
- if (goods == null || goods.getStock() <= 0) {
- return false; // 商品不存在或库存不足,返回秒杀失败
- }
- goods.setStock(goods.getStock() - 1);
- goodsRepository.save(goods);
- return true; // 库存扣减成功,返回秒杀成功
- } finally {
- RedisLockUtil.unlock(redisTemplate, lockKey); // 释放锁
- }
- }
- @Override
- @Transactional
- public SeckillOrder createOrder(Long userId, Long goodsId) {
- SeckillOrder order = new SeckillOrder();
- order.setUserId(userId);
- order.setGoodsId(goodsId);
- order.setOrderPrice(getGoodsById(goodsId).getPrice());
- order.setOrderStatus(1); // 假设订单状态为1表示已支付
- return orderRepository.save(order);
- }
- }
复制代码 Redis分布式锁工具类
创建Redis分布式锁工具类:
- package com.example.seckill.util;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.stereotype.Component;
- import java.util.concurrent.TimeUnit;
- @Component
- public class RedisLockUtil {
- public static boolean tryLock(StringRedisTemplate redisTemplate, String lockKey, int expireTime) {
- Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", expireTime, TimeUnit.SECONDS);
- return Boolean.TRUE.equals(result);
- }
- public static void unlock(StringRedisTemplate redisTemplate, String lockKey) {
- redisTemplate.delete(lockKey);
- }
- }
复制代码 Controller层
创建秒杀控制器:
- package com.example.seckill.controller;
- import com.example.seckill.entity.SeckillGoods;
- import com.example.seckill.entity.SeckillOrder;
- import com.example.seckill.service.SeckillService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.*;
- @RestController
- @RequestMapping("/seckill")
- public class SeckillController {
- @Autowired
- private SeckillService seckillService;
- @GetMapping("/goods/{id}")
- public SeckillGoods getGoodsById(@PathVariable Long id) {
- return seckillService.getGoodsById(id);
- }
- @PostMapping("/seckill/{id}")
- public String seckill(@PathVariable Long id, @RequestParam Long userId) {
- if (seckillService.reduceStock(id)) {
- SeckillOrder order = seckillService.createOrder(userId, id);
- return "秒杀成功,订单ID:" + order.getId();
- } else {
- return "秒杀失败,库存不足";
- }
- }
- }
复制代码 启动类
创建Spring Boot启动类:
- package com.example.seckill;
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- @SpringBootApplication
- public class SeckillApplication {
- public static void main(String[] args) {
- SpringApplication.run(SeckillApplication.class, args);
- }
- }
复制代码 实战重现
数据库初始化
在MySQL数据库中创建商品和订单表,并插入初始数据:
- CREATE TABLE seckill_goods (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- name VARCHAR(255) NOT NULL,
- stock INT NOT NULL,
- price INT NOT NULL
- );
- CREATE TABLE seckill_order (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- user_id BIGINT NOT NULL,
- goods_id BIGINT NOT NULL,
- order_price INT NOT NULL,
- order_status INT NOT NULL
- );
- INSERT INTO seckill_goods (name, stock, price) VALUES ('iPhone 14', 10, 4999);
复制代码 启动项目
启动Spring Boot项目,确保Redis和MySQL服务已经启动。
模拟秒杀哀求
利用Postman或curl等工具模拟秒杀哀求,例如:
- bash复制代码
- curl -X POST "http://localhost:8080/seckill/seckill/1?userId=123456"
复制代码 观察结果
在秒杀过程中,观察Redis缓存中的库存变化以及MySQL数据库中订单表的记载。确保在高并发场景下不会出现超卖问题。
总结
本文通过背景汗青、业务场景、底层原理以及Java代码实现等方面具体先容了高并发场景秒杀抢购超卖Bug的实战重现。通过利用Redis分布式锁和数据库乐观锁等本事,可以有效防止超卖问题的发生。同时,通过缓存优化和消息队列削峰填谷等计谋,可以进一步进步系统的并发处理本事和稳定性。盼望本文可以或许资助开辟者更好地理解和解决秒杀抢购超卖Bug问题。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |