秒杀系统学习笔记(编程不良人)

本文介绍了如何在Spring Boot应用中通过乐观锁和令牌桶算法实现库存控制的秒杀功能,包括数据库设计、Service层的事务管理和并发控制,以及使用Guava RateLimiter进行接口限流。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

创建数据库

-- ----------------------------
-- Table structure for stock
-- ----------------------------
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
  `count` int(11) NOT NULL COMMENT '库存',
  `sale` int(11) NOT NULL COMMENT '已售',
  `version` int(11) NOT NULL COMMENT '乐观锁,版本号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Table structure for stock_order
-- ----------------------------
DROP TABLE IF EXISTS `stock_order`;
CREATE TABLE `stock_order` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `sid` int(11) NOT NULL COMMENT '库存ID',
  `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

编写代码

Controller层

@RequestMapping("stock")
@RestController
public class StockController {
 
    @Autowired
    private OrderService orderService;
 
    //开发秒杀方法
    public String kill(Integer id) {
        System.out.println("id = " + id);
        try {
            //根据秒杀商品的id调用秒杀业务
            int orderID = orderService.kill(id);
            return "秒杀成功,订单id为" + String.valueOf(orderID);
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
 
 
    }
}

Service层

@Service
@Transactional
public class OrderServiceImpl implements OrderService {
 
    @Autowired
    private StockDao stockDao;
 
    @Autowired
    private OrderDao orderDao;
 
    @Override
    public int kill(Integer id) {
        //根据商品id校验库存
        Stock stock = stockDao.checkStock(id);
        if (stock.getSale().equals(stock.getCount())) {
            throw new RuntimeException("库存不足!");
        } else {
            //扣库存
            stock.setSale(stock.getSale() + 1);
            stockDao.updateSale(stock);
            //创建订单
            Order order = new Order();
            order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());
            orderDao.createOrder(order);
            return order.getId();
        }
 
    }
}

Dao层

public interface StockDao {
    //根据商品的id查询库存信息
    Stock checkStock(Integer id);
 
    //根据商品id扣除库存
    void updateSale(Stock stock);
}
public interface OrderDao {
    // 创建订单
    void createOrder(Order order);
}

安装Jmeter 下载页面

使用同步锁解决超卖问题

不能在方法上面加synchronized关键字,因为在类上已加了事务注解,也会有一个线程同步,而事务的范围比代码块的范围大。
可能会出现多提交的问题,因此不能在方法上加synchronized关键字。
解决:
1.把transactional事务注解去掉,方法加上synchronized关键字
2.在需要同步的地方使用代码块,在controller类里面使用,保证同步的范围比事务的大即可。
@RequestMapping("stock")
@RestController
public class StockController {
 
    @Autowired
    private OrderService orderService;
 
    //开发秒杀方法
    @RequestMapping("/kill")
    public String kill(Integer id) {
        System.out.println("id = " + id);
        try {
            synchronized (this) {
                //根据秒杀商品的id调用秒杀业务
                int orderID = orderService.kill(id);
                return "秒杀成功,订单id为" + String.valueOf(orderID);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
 
 
    }
}
package com.lany.miaosha.service;
 
import com.lany.miaosha.dao.OrderDao;
import com.lany.miaosha.dao.StockDao;
import com.lany.miaosha.entity.Order;
import com.lany.miaosha.entity.Stock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import java.util.Date;
 
/**
 * @author gdh
 * @description
 * @date 2022年2月16日 15:35
 */
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
 
    @Autowired
    private StockDao stockDao;
 
    @Autowired
    private OrderDao orderDao;
 
    /*
        不能在方法上面加synchronized关键字,因为在类上已加了事务注解,也会有一个线程同步,而事务的范围比代码块的范围大。
        可能会出现多提交的问题,因此不能在方法上加synchronized关键字。
        解决:
        1.把transactional事务注解去掉,方法加上synchronized关键字
        2.在需要同步的地方使用代码块,在controller类里面使用,保证同步的范围比事务的大即可。
     */
    @Override
    public int kill(Integer id) {
        //校验库存
        Stock stock = checkStock(id);
        //更新库存
        updateSale(stock);
        //创建订单
        return createOrder(stock);
    }
 
    //校验库存
    private Stock checkStock(Integer id) {
        Stock stock = stockDao.checkStock(id);
        if (stock.getSale().equals(stock.getCount())) {
            throw new RuntimeException("库存不足!!");
        }
        return stock;
    }
 
    //扣除库存
    private void updateSale(Stock stock) {
        stock.setSale(stock.getSale() + 1);
        stockDao.updateSale(stock);
    }
 
    //创建订单
    private Integer createOrder(Stock stock) {
        Order order = new Order();
        order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());
        orderDao.createOrder(order);
        return order.getId();
    }
 
}

使用乐观锁解决数据超卖的问题

使用乐观锁解决商品超卖问题,实际上是把防止超卖问题交给数据库解决,利用数据库表中定义的version字段以及数据库中的事务实现在并发情况下商品的超卖问题。

扣除库存方法改造

只改变校验库存的代码,其他方法的代码不变

//扣除库存
    private void updateSale(Stock stock) {
        //在sql层面完成销量的+1,和版本号+1,并且根据商品id和版本号查询更新的商品
        int updateRows = stockDao.updateSale(stock);
        if (updateRows == 0) {
            throw new RuntimeException("抢购失败,请重试");
        }
    }
public interface StockDao {
    //根据商品的id查询库存信息
    Stock checkStock(Integer id);
 
    // 根据商品id扣除库存
    // void updateSale(Stock stock);
    int updateSale(Stock stock);
}
<update id="updateSale" parameterType="Stock">
    update stock
    set sale = sale + 1,
    version = version + 1
    where id = #{id}
        and version=#{version}
</update>

完整的业务方法

OrderServiceImpl.java

@Service
@Transactional
public class OrderServiceImpl implements OrderService {
 
    @Autowired
    private StockDao stockDao;
 
    @Autowired
    private OrderDao orderDao;
 
    @Override
    public int kill(Integer id) {
 
        //校验库存
        Stock stock = checkStock(id);
        //更新库存
        updateSale(stock);
        //创建订单
        return createOrder(stock);
    }
 
    //校验库存
    private Stock checkStock(Integer id) {
        Stock stock = stockDao.checkStock(id);
        if (stock.getSale().equals(stock.getCount())) {
            throw new RuntimeException("库存不足!!");
        }
        return stock;
    }
 
    //扣除库存
    private void updateSale(Stock stock) {
        //在sql层面完成销量的+1,和版本号+1,并且根据商品id和版本号查询更新的商品
        int updateRows = stockDao.updateSale(stock);
        if (updateRows == 0) {
            throw new RuntimeException("抢购失败,请重试");
        }
    }
 
    //创建订单
    private Integer createOrder(Stock stock) {
        Order order = new Order();
        order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());
        orderDao.createOrder(order);
        return order.getId();
    }
 
}

StockDaoMapper.xml

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.lany.miaosha.dao.StockDao">
    <!--根据秒杀商品id查询库存-->
    <select id="checkStock" resultType="Stock" parameterType="int">
        select id,name,count,sale,version from stock
        where id = #{id}
    </select>
 
    <!--根据商品id扣除库存-->
    <update id="updateSale" parameterType="Stock">
        update stock
        set sale = sale + 1,
        version = version + 1
        where id = #{id}
            and version=#{version}
    </update>
 
</mapper>

接口限流
限流:就是对某一时间内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致系统运行缓慢或宕机。

什么是接口限流
在面临高并发的抢购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力。大量的请求抢购成功时需要调用下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。

如何解决接口限流
常用的接口限流算法有令牌桶和漏斗算法,Google的开源项目Guava中的RateLimiter使用的就是令牌桶算法。

在开发高并发系统时有三把利器用来保护系统:缓存、降级、限流

令牌桶算法和漏斗算法
漏斗算法:请求打到服务器之后,进入漏桶中,漏桶以一定的速度处理请求,当请求量过大时,导致漏桶直接溢出时,可以看出漏桶算法可以限制数据的传世速率。
令牌桶算法:在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,是流量比较均匀的速度向外发送。令牌桶逆算法实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,知道把桶填满。后面在产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。这意味,面对瞬时大流量,该算法可以在段时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗恨到的事情。
令牌桶的简单使用
1.引入Guava依赖

<!--Google开源工具包RateLimter实现令牌桶-->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.2-jre</version>
</dependency>

2.创建令牌桶实例

在controller类中创建令牌桶的实例

//创建令牌桶实例
private RateLimiter rateLimiter = RateLimiter.create(10);

3.使用令牌桶算法

@GetMapping("/sale")
public String sale(Integer id) {
    //1.没有获取到token的请求阻塞直到获取到token令牌。
    //log.info("等待时间:" + rateLimiter.acquire());
    //2.设置一个等待时间,如果在等待时间内获取到了令牌,则处理业务,如果在等待时间内没有获取到相应的token,则抛弃请求。
    if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)) {
        System.out.println("当前请求被限流,直接抛弃,无法调用后续秒杀逻辑....");
        return "抢购失败";
    }
    System.out.println("处理业务...............");
 
    return "抢购成功";
}

4.使用JMeter测试

使用令牌桶算法实现乐观锁+限流

@RequestMapping("stock")
@RestController
@Slf4j
public class StockController {
 
    @Autowired
    private OrderService orderService;
 
    //创建令牌桶实例
    private RateLimiter rateLimiter = RateLimiter.create(20);
 
    @RequestMapping("/killtoken")
    public String killtoken(Integer id) {
        System.out.println("id = " + id);
        //加入令牌桶限流措施
        if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)) {
            return "抢购失败,当前秒杀活动过于火爆,请重试!";
        }
        try {
            //根据秒杀商品的id调用秒杀业务
            int orderID = orderService.kill(id);
            return "秒杀成功,订单id为" + String.valueOf(orderID);
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }

参考:

编程不良人

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值