本文分享自华为云社区《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 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 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> getOrderListByUserId(@RequestParam(“userId”) Long userId) {

    List 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);
}
}
}

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