创建数据库
-- ----------------------------
-- 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);
}
使用同步锁解决超卖问题
不能在方法上面加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();
}
}
参考:
编程不良人