摘要:本博客将介绍如何使用 Spring Boot 实现一个简单的商城秒杀系统,并通过使用 Redis 和 MySQL 来增强其性能和可靠性。
本文分享自华为云社区《Spring Boot实现商城高并发秒杀案例》,作者:林欣。
随着经济的发展和人们消费观念的转变,电子商务逐渐成为人们购物的主要方式之一。高并发是电子商务网站面临的一个重要挑战。本博客将介绍如何使用 Spring Boot 实现一个简单的商城秒杀系统,并通过使用 Redis 和 MySQL 来增强其性能和可靠性。
准备工作
在开始之前,您需要准备以下工具和环境:
- JDK 1.8 或更高版本
- Redis
- MySQL
- MyBatis
实现步骤
步骤一:创建数据库
首先,我们需要创建一个数据库来存储商品信息、订单信息和秒杀活动信息。在这里,我们使用 MySQL 数据库,创建一个名为 shop 的数据库,并建立三个表 goods、order 和 seckill。
表 goods 存储了所有的商品信息,包括商品编号、名称、描述、价格和库存数量等等。
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`name` varchar(50) NOT NULL COMMENT '商品名称',
`description` varchar(100) NOT NULL COMMENT '商品描述',
`price` decimal(10,2) NOT NULL COMMENT '商品价格',
`stock_count` int(11) NOT NULL COMMENT '商品库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
表 order 存储了所有的订单信息,包括订单编号、用户ID、商品ID、秒杀活动ID 和订单状态等等。
CREATE TABLE `order` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
`goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',
`seckill_id` BIGINT(20) DEFAULT NULL COMMENT '秒杀活动ID',
`status` TINYINT(4) NOT NULL COMMENT '订单状态,0-未支付,1-已支付,2-已发货,3-已收货,4-已退款,5-已完成',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_order` (`user_id`,`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
表 seckill 存储了所有的秒杀活动信息,包括秒杀活动编号、商品ID、开始时间和结束时间等等。
CREATE TABLE `seckill` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀活动ID',
`goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',
`start_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间',
`end_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '结束时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀活动表';
步骤二:创建 Spring Boot 项目
接下来,我们需要创建一个 Spring Boot 项目,用于实现商城高并发秒杀案例。可以使用 Spring Initializr 来快速创建一个基本的 Spring Boot 项目。
步骤三:配置 Redis 和 MySQL
在 Spring Boot 项目中,我们需要配置 Redis 和 MySQL 的连接信息。可以在 application.properties 文件中设置以下属性:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shop?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=123456
步骤四:编写实体类和 DAO 接口
在这一步中,我们需要定义三个实体类分别对应数据库中的 goods、order 和 seckill 表。同时,我们需要编写相应的 DAO 接口,用于操作这些实体类。
// 商品实体类
@Data
public class Goods {
private Long id;
private String name;
private String description;
private BigDecimal price;
private Integer stockCount;
}
// 商品 DAO 接口
@Mapper
public interface GoodsDao {
@Select("SELECT * FROM goods WHERE id = #{id}")
Goods getGoodsById(Long id);
@Update("UPDATE goods SET stock_count = stock_count - 1 WHERE id = #{id} AND stock_count > 0")
int reduceStockCount(Long id);
}
// 订单实体类
@Data
public class Order {
private Long id;
private Long userId;
private Long goodsId;
private Long seckillId;
private Byte status;
private Date createTime;
private Date updateTime;
}
// 订单 DAO 接口
@Mapper
public interface OrderDao {
@Select("SELECT * FROM `order` WHERE user_id = #{userId} AND goods_id = #{goodsId}")
Order getOrderByUserIdAndGoodsId(@Param("userId") Long userId, @Param("goodsId") Long goodsId);
@Insert("INSERT INTO `order` (user_id, goods_id, seckill_id, status, create_time, update_time) VALUES (#{userId}, #{goodsId}, #{seckillId}, #{status},#{createTime},#{updateTime})")
int insertOrder(Order order);
@Select("SELECT o.*, g.name, g.price FROM `order` o LEFT JOIN goods g ON o.goods_id = g.id WHERE o.user_id = #{userId}")
List<OrderVo> getOrderListByUserId(Long userId);
}
// 秒杀活动实体类
@Data
public class Seckill {
private Long id;
private Long goodsId;
private Date startTime;
private Date endTime;
}
// 秒杀活动 DAO 接口
@Mapper
public interface SeckillDao {
@Select("SELECT * FROM seckill WHERE id = #{id}")
Seckill getSeckillById(Long id);
@Update("UPDATE seckill SET end_time = #{endTime} WHERE id = #{id}")
int updateSeckillEndTime(@Param("id") Long id, @Param("endTime") Date endTime);
}
步骤五:编写 Service 层和 Controller
在这一步中,我们需要编写 Service 层和 Controller 类,用于实现商城高并发秒杀案例的核心功能。
- 商品 Service 层:用于获取商品信息和减少商品库存数量。
@Service
public class GoodsService {
private final GoodsDao goodsDao;
@Autowired
public GoodsService(GoodsDao goodsDao) {
this.goodsDao = goodsDao;
}
public Goods getGoodsById(Long id) {
return goodsDao.getGoodsById(id);
}
public boolean reduceStockCount(Long id) {
return goodsDao.reduceStockCount(id) > 0;
}
}
- 订单 Service 层:用于创建订单和获取订单信息。
@Service
public class OrderService {
private final OrderDao orderDao;
@Autowired
public OrderService(OrderDao orderDao) {
this.orderDao = orderDao;
}
public Order createOrder(Long userId, Long goodsId, Long seckillId) {
Order order = new Order();
order.setUserId(userId);
order.setGoodsId(goodsId);
order.setSeckillId(seckillId);
order.setStatus((byte) 0);
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
orderDao.insertOrder(order);
return order;
}
public List<OrderVo> getOrderListByUserId(Long userId) {
return orderDao.getOrderListByUserId(userId);
}
}
- 秒杀活动 Service 层:用于获取秒杀活动信息和更新秒杀活动结束时间。
@Service
public class SeckillService {
private final SeckillDao seckillDao;
@Autowired
public SeckillService(SeckillDao seckillDao) {
this.seckillDao = seckillDao;
}
public Seckill getSeckillById(Long id) {
return seckillDao.getSeckillById(id);
}
public boolean updateSeckillEndTime(Long id, Date endTime) {
return seckillDao.updateSeckillEndTime(id, endTime) > 0;
}
}
- 订单 Controller:用于处理订单相关的请求。
@RestController
@RequestMapping("/order")
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/create")
public CommonResult<Order> createOrder(@RequestParam("userId") Long userId,
@RequestParam("goodsId") Long goodsId,
@RequestParam("seckillId") Long seckillId) {
Order order = orderService.createOrder(userId, goodsId, seckillId);
if (order == null) {
return CommonResult.failed(ResultCode.FAILURE);
}
return CommonResult.success(order);
}
@GetMapping("/list")
public CommonResult<List<OrderVo>> getOrderListByUserId(@RequestParam("userId") Long userId) {
List<OrderVo> orderList = orderService.getOrderListByUserId(userId);
return CommonResult.success(orderList);
}
}
秒杀活动 Controller:用于处理秒杀活动相关的请求。
@RestController
@RequestMapping("/seckill")
public class SeckillController {
private final SeckillService seckillService;
private final GoodsService goodsService;
private final OrderService orderService;
@Autowired
public SeckillController(SeckillService seckillService, GoodsService goodsService, OrderService orderService) {
this.seckillService = seckillService;
this.goodsService = goodsService;
this.orderService = orderService;
}
@PostMapping("/start")
public CommonResult<Object> startSeckill(@RequestParam("userId") Long userId,
@RequestParam("goodsId") Long goodsId,
@RequestParam("seckillId") Long seckillId) {
// 查询秒杀活动是否有效
Seckill seckill = seckillService.getSeckillById(seckillId);
if (seckill == null || seckill.getStartTime().after(new Date()) || seckill.getEndTime().before(new Date())) {
return CommonResult.failed(ResultCode.FAILURE, "秒杀活动不存在或已结束");
}
// 判断商品库存是否充足
Goods goods = goodsService.getGoodsById(goodsId);
if (goods == null || goods.getStockCount() <= 0) {
return CommonResult.failed(ResultCode.FAILURE, "商品库存不足");
}
// 生成订单
Order order = orderService.createOrder(userId, goodsId, seckillId);
if (order == null) {
return CommonResult.failed(ResultCode.FAILURE, "订单创建失败,请稍后再试");
}
// 减少商品库存
boolean success = goodsService.reduceStockCount(goodsId);
if (!success) {
return CommonResult.failed(ResultCode.FAILURE, "减少商品库存失败,请稍后再试");
}
return CommonResult.success("秒杀成功");
}
}
步骤六:使用 Redis 实现分布式锁
在商城高并发秒杀案例中,一个重要的问题是如何保证商品库存数量的一致性和秒杀结果的正确性。为了解决这个问题,我们可以使用 Redis 实现分布式锁。
在 RedisService 类中实现分布式锁:
@Service
public class RedisService {
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public RedisService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public boolean lock(String key, String value, long expire) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(expire));
return result != null && result;
}
public void unlock(String key, String value) {
if (value.equals(redisTemplate.opsForValue().get(key))) {
redisTemplate.delete(key);
}
}
}
在 SeckillService 中使用分布式锁实现秒杀接口:
@Service
public class SeckillService {
private final RedisService redisService;
private final SeckillDao seckillDao;
private final GoodsDao goodsDao;
private final OrderDao orderDao;
@Autowired
public SeckillService(RedisService redisService, SeckillDao seckillDao, GoodsDao goodsDao, OrderDao orderDao) {
this.redisService = redisService;
this.seckillDao = seckillDao;
this.goodsDao = goodsDao;
this.orderDao = orderDao;
}
public CommonResult<Object> startSeckill(Long userId, Long goodsId, Long seckillId) {
String lockKey = "seckill:lock:" + goodsId;
String lockValue = UUID.randomUUID().toString();
try {
// 获取分布式锁
if (!redisService.lock(lockKey, lockValue, 10)) {
return CommonResult.failed(ResultCode.FAILURE, "当前请求太过频繁,请稍后再试");
}
// 查询秒杀活动是否有效
Seckill seckill = seckillDao.getSeckillById(seckillId);
if (seckill == null || seckill.getStartTime().after(new Date()) || seckill.getEndTime().before(new Date())) {
return CommonResult.failed(ResultCode.FAILURE, "秒杀活动不存在或已结束");
}
// 判断商品库存是否充足
Goods goods = goodsDao.getGoodsById(goodsId);
if (goods == null || goods.getStockCount() <= 0) {
return CommonResult.failed(ResultCode.FAILURE, "商品库存不足");
}
// 创建订单
Order order = new Order();
order.setUserId(userId);
order.setGoodsId(goodsId);
order.setSeckillId(seckillId);
order.setStatus((byte) 0);
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
int count = orderDao.insertOrder(order);
if (count <= 0) {
return CommonResult.failed(ResultCode.FAILURE, "订单创建失败,请稍后再试");
}
// 减少商品库存
boolean success = goodsDao.reduceStockCount(goodsId) > 0;
if (!success) {
throw new Exception("减少商品库存失败,请稍后再试");
}
return CommonResult.success("秒杀成功");
} catch (Exception e) {
e.printStackTrace();
return CommonResult.failed(ResultCode.FAILURE, "秒杀失败," + e.getMessage());
} finally {
// 释放分布式锁
redisService.unlock(lockKey, lockValue);
}
}
}