摘要:本博客将介绍如何使用 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);
}
}
}

点击关注,第一时间了解华为云新鲜技术~

6步带你用Spring Boot开发出商城高并发秒杀系统的更多相关文章

  1. 使用Spring Boot开发Web项目(二)之添加HTTPS支持

    上篇博客使用Spring Boot开发Web项目我们简单介绍了使用如何使用Spring Boot创建一个使用了Thymeleaf模板引擎的Web项目,当然这还远远不够.今天我们再来看看如何给我们的We ...

  2. 使用Spring boot开发RestFul 风格项目PUT/DELETE方法不起作用

    在使用Spring boot 开发restful 风格的项目,put.delete方法不起作用,解决办法. 实体类Student @Data public class Student { privat ...

  3. Spring Boot 开发系列一 开发环境的一些九九

    从今天开始写这个Spring Boot 开发系列,我是第二周学习JAVA的,公司号称springboot把JAVA的开发提升到填空的能力,本人是NET转JAVA的,想看看这个填空的东西到底有多强.废话 ...

  4. Spring Boot开发HTTPS协议的REST接口

    Spring Boot开发HTTP的REST接口流程在前文中已经描述过,见<SpringBoot开发REST接口>. 如需要支持HTTPS,只需要在如上基础上进行设置.修改/resourc ...

  5. Spring Boot 开发微信公众号后台

    Hello 各位小伙伴,松哥今天要和大家聊一个有意思的话题,就是使用 Spring Boot 开发微信公众号后台. 很多小伙伴可能注意到松哥的个人网站(http://www.javaboy.org)前 ...

  6. 天天玩微信,Spring Boot 开发私有即时通信系统了解一下

    1/ 概述 利用Spring Boot作为基础框架,Spring Security作为安全框架,WebSocket作为通信框架,实现点对点聊天和群聊天. 2/ 所需依赖 Spring Boot 版本 ...

  7. spring boot开发,jar包一个一个来启动太麻烦了,写一个bat文件一键启动

    spring boot开发,jar包一个一个来启动太麻烦了,写一个bat文件一键启动 @echo offcd D:\workProject\bushustart cmd /c "title ...

  8. spring boot 开发环境搭建(Eclipse)

    Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...

  9. Spring Boot 开发集成 WebSocket,实现私有即时通信系统

    1/ 概述 利用Spring Boot作为基础框架,Spring Security作为安全框架,WebSocket作为通信框架,实现点对点聊天和群聊天. 2/ 所需依赖 Spring Boot 版本 ...

  10. Spring Boot入门系列(十五)Spring Boot 开发环境热部署

    在实际的项目开发过中,当我们修改了某个java类文件时,需要手动重新编译.然后重新启动程序的,整个过程比较麻烦,特别是项目启动慢的时候,更是影响开发效率.其实Spring Boot的项目碰到这种情况, ...

随机推荐

  1. pyahocorasick 安装和使用问题总结

    因系统中用到了ahocorasick,但是程序跑起来有BUG,故而10.1假期研究了一下,趟过几个坑,分享一下. 一.安装过程中的坑 直接安装pip install  pyahocorasick 是会 ...

  2. Linux_GItlab

    Gitlab实战 Gitlab安装 Gitlab简单使用 配置Jenkins 向 Gitlab 拉取代码 配置gitlab 触发器 Gitlab安装 环境需求: 一台干净的新机器(防止端口冲突) 配置 ...

  3. 5、什么是MIME

    MIME 类型 媒体类型(通常称为 Multipurpose Internet Mail Extensions 或 MIME 类型 )是一种标准,用来表示文档.文件或字节流的性质和格式.它在IETF ...

  4. Java 方法详解 与数组

    基础阶段: 1.何谓方法 何谓方法?◆System.out.println(),那么它是什么呢?◆Java方法是语句的集合,它们在- -起执行一个功能.   ◆方法是解决一类问题的步骤的有序组合    ...

  5. ES2015常用知识点

    ES2015(又称ES6)部分1 let/const以及块作用域:2 循环语句 const arr=[1,2,3]; for(const item of arr){ console.log(item) ...

  6. (一).JavaScript的简介,变量,数据类型,运算符和表达式

    1. JavaScript的简介 1.1 JavaScript概念 JavaScript是一门:动态的 弱类型的 解释型 的脚本语言 1. 动态: 程序执行的时候才确定数据类型 2. 弱类型:数据类型 ...

  7. Rfost的自我介绍+软工五问

    自我介绍+软工五问 问题 解答 这个作业属于哪个课程 网工1934-软件工程 这个作业要求在哪里 作业要求 这个作业的目标 让学生对软件工程有个初步的了解,同时掌握基础的markdown语法和博客园的 ...

  8. Delaunay triangulation 的实现

    在GitHub 找到的别人的代码:https://github.com/earthwjl/DelaunayTriangulate 解压后是这样的:(没有x64) 直接就有了.sln工程文件,于是用Vi ...

  9. Unity泛型单例模式

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class Singleto ...

  10. List<dto> 转List<map>

    /** * list<DTO> 转 list<Map<String,Object>> * * @param list * @param <T> * @r ...