30min左右可以回看
慢查询
登录服务器,进入mysql,开启慢查询日志,设置慢查询时间
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show variables like 'slow_query%'; mysql> show variables like 'long_query%'; mysql> set global slow_query_log='ON'; mysql> set global long_query_time=1;
退出重进mysql才生效。
mysql> quit;
再次登录mysql,查询一个大于生效时间
mysql> select sleep(3)
执行完sql退出mysql,调用慢查询日志,可看到超时的sql语句
查询功能优化的数据库方面:慢查询分析
先启用慢查询日志,发现某SQL大于xx秒,觉得比较慢,explain去看语法结构,发现没走索引。
可能没加索引;加了索引但是没走,可能条件跟最左前缀原则不匹配。或者子查询的分页分组写的不好。
超卖问题
下单时锁库存
- 下单时锁库存
- 业务层面解决超卖问题。(不会超卖,可能会少卖)
- 超时自动释放订单,回补库存
- 主流方法
- 付款时锁库存
- 会超卖
- 体验不好
- 库存很多时,选这种
一般都是下单时锁库存,方法一
表分析
order_info订单表:字段
- 订单id
- 用户id
- 商品id
- 商品参加活动id
- 冗余记录订单单价
- 商品数量
- 订单总金额
订单id:日期+流水号
- 便于业务的表拆分,把xx范围的日期订单,拆分出去到历史表。
- 流水号恒自增,持久性存储。放数据库,不能放内存中。
serial_number表
- 流水号表,记录订单索引的最大序号
- 字段:name,什么什么的索引序号。可以扩展,记录别的表的最大数量
- value,索引序号最大值
- step:步长,每次增加x个订单。提高业务灵活性
代码分析
数据访问层
mybatis插件,自动生成的OrderMapper、ItemMapper、ItemStockMapper、SerialNumberMapper的dao增删改查接口,以及mapper包下的.xml的sql方法。
ItemMapper加一个,增加销量方法increaseSales(Integer id, Integer amount),ItemStockMapper加一个减少库存方法decreaseStock(Integer itemId, Integer amount) id匹配,库存要大于减少的数量
SerialNumberMapper.xml的sql中,根据主键查询最大索引(订单)序号的方法。
sql结尾加 for update,排他锁。
因为查询目的,要立马修改数据,所以加锁,防止其他线程访问
业务层service【主要看serviceimpl实现类】
- 实现类中减库存逻辑,不用判断库存够不够,直接扣,根据返回值的T/F反应减扣成功还是库存不够
- 库存不够,mapper.xml文件的sql查不到数据,返回的rows=0,返回F
@Transactional
public boolean decreaseStock(int itemId, int amount) {
int rows = itemStockMapper.decreaseStock(itemId, amount);
return rows > 0;
}
创建订单:
- 传入活动参数,不为空但是小于零,活动有问题。防止传入奇怪参数被攻击。(只为空表示不参与活动)
- 校验用户是否存在、同理商品
- 校验库存,判断一下库存是否大于购买数量
- 校验活动,活动id不为空继续,在上面校验商品item时,会查活动对象。
- 校验item活动对象,没有-无活动。
- 商品的活动id跟传入的活动id不匹配,抛异常
- 活动状态码不为0,抛异常
=====================================================
-
参数都没问题后,开始扣减库存。下单时锁库存,所以先扣库存,扣完后锁住。
-
调用缄口库存方法,不成功报错,成功继续
-
生成订单
-
更新销量
@Transactional
public Order createOrder(int userId, int itemId, int amount, Integer promotionId) {
// 校验参数
if (amount < 1 || (promotionId != null && promotionId.intValue() <= 0)) {
throw new BusinessException(PARAMETER_ERROR, "指定的参数不合法!");
}
// 校验用户
User user = userService.findUserById(userId);
if (user == null) {
throw new BusinessException(PARAMETER_ERROR, "指定的用户不存在!");
}
// 校验商品
Item item = itemService.findItemById(itemId);
if (item == null) {
throw new BusinessException(PARAMETER_ERROR, "指定的商品不存在!");
}
// 校验库存
int stock = item.getItemStock().getStock();
if (amount > stock) {
throw new BusinessException(STOCK_NOT_ENOUGH, "库存不足!");
}
// 校验活动
if (promotionId != null) {
if (item.getPromotion() == null) {
throw new BusinessException(PARAMETER_ERROR, "指定的商品无活动!");
} else if (!item.getPromotion().getId().equals(promotionId)) {
throw new BusinessException(PARAMETER_ERROR, "指定的活动不存在!");
} else if (item.getPromotion().getStatus() == 1) {
throw new BusinessException(PARAMETER_ERROR, "指定的活动未开始!");
}
}
// 扣减库存
boolean successful = itemService.decreaseStock(itemId, amount);
if (!successful) {
throw new BusinessException(STOCK_NOT_ENOUGH, "库存不足!");
}
// 生成订单
Order order = new Order();
order.setId(this.generateOrderID());
order.setUserId(userId);
order.setItemId(itemId);
order.setPromotionId(promotionId);
order.setOrderPrice(promotionId != null ? item.getPromotion().getPromotionPrice() : item.getPrice());
order.setOrderAmount(amount);
order.setOrderTotal(order.getOrderPrice().multiply(new BigDecimal(amount)));
order.setOrderTime(new Timestamp(System.currentTimeMillis()));
orderMapper.insert(order);
// 更新销量
itemService.increaseSales(itemId, amount);
return order;
}
还有生成订单流水号
-
更新流水号是获取目前库存最大值,在加上步长,之后的就是订单流水号
-
前面拼12个0,要除去占的位数后
@Transactional(propagation = Propagation.REQUIRES_NEW)
private String generateOrderID() {
StringBuilder sb = new StringBuilder();
// 拼入日期
sb.append(Toolbox.format(new Date(), "yyyyMMdd"));
// 获取流水号
SerialNumber serial = serialNumberMapper.selectByPrimaryKey("order_serial");
Integer value = serial.getValue();
// 更新流水号
serial.setValue(value + serial.getStep());
serialNumberMapper.updateByPrimaryKey(serial);
// 拼入流水号
String prefix = "000000000000".substring(value.toString().length());
sb.append(prefix).append(value);
return sb.toString();
}
下单的controller
-
从session取得当前用户,创建订单,传入订单信息。
-
正常就返回正常的编码,出错由拦截器拦截异常
@Controller
@RequestMapping("/order")
@CrossOrigin(origins = "${nowcoder.web.path}", allowedHeaders = "*", allowCredentials = "true")
public class OrderController implements ErrorCode {
@Autowired
private OrderService orderService;
@RequestMapping(path = "/create", method = RequestMethod.POST)
@ResponseBody
public ResponseModel create(
HttpSession session, int itemId, int amount, Integer promotionId) {
User user = (User) session.getAttribute("loginUser");
orderService.createOrder(user.getId(), itemId, amount, promotionId);
return new ResponseModel();
}
}
小结
-
下单时锁库库存,业务层面解决超卖问题
-
后期订单很多,要把历史订单数据拆分出去,所以订单id采用日期+流水号
-
流水号表记录了最大订单数,每次查此表,会在select的sql语句后加for update排他锁,锁住。避免别的事务访问
事务(基于MySQL的InnoDB引擎)
原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration)
ACID
- 原子性(Atomicity):事务是最小的执行单位。确保了动作要么全部完成,要么全不起作用
- 一致性(Consistency):执行事务前后,数据保持一致。
- 隔离性(Isolation):并发访问数据库时,一个用户的事务不受其他事务影响。
各并发事务之间数据库是独立的。 - 持久性(Duration):事务提交后,对数据库的更改是持久的。
InnoDB主键索引就是聚簇索引,用主键索引访问数据不用回表,因为数据都存在其主键(聚簇)索引叶子节点上
隔离性
粒度
- 表锁:针对非索引字段加的锁
- 行锁:针对索引字段加的锁
- InnoDB中底层都是页锁(区锁)
锁类型
- 共享锁 S :行锁,允许读一行数据
- 排他锁 X :行锁,允许改一行数据
- 意向共享锁 IS :表锁,想要/准备读表中的数据
- 意向排他锁 IX :表锁,想要/准备改表中的数据
注:
- X最重:与其他所有锁互斥
- IX第二重:与行锁S、X互斥
以前加X锁丧失并发性,现在不会了因为MVCC
底层机制(提升并发性)
无锁:
-
底层能不加锁就不加锁
-
通过MVCC机制实现
-
MVCC:多版本并发控制- 对于正在更新的数据,InnoDB会去读取该行的一个快照数据(undo log)。
- 每次提交事务,日志(undo log)会记录历史版本数据。
- 事务一更新数据加了排他X锁,事务二可以读数据,但是读的历史版本数据。
- 能不能读undo log要看隔离级别。
加锁:
-
查询时可以显式加锁
-
X锁:SELECT ... FOR UPDATE
-
S锁:SELECT ... LOCK IN SHARE MODE
算法
InnoDB的三种行锁/三种算法实现锁机制:
- 记录锁/Record Lock:锁行
- 间隙锁/Gap Lock:锁定一个范围,但不包含记录本身
- 临键锁/Next-Key Lock:锁定一个范围,并且锁定记录本身。上面两者综合
注意:
- 记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
- 临键锁解决幻读问题
读取/更新
- 脏读:事务a读取到事务b更新但未提交的数据(不能接受)
- 幻读:事务期间,对某数据前后查询行数(记录数量)不一致(范围变化,还可以接受)
- 不可重复读:事务对同一数据前后读取结果不一致(勉强接受)
例子
- 第一类丢失更新:事务a回滚,导致事务b已更新(修改)数据丢失
- 第二类丢失更新:事务a提交,导致事务b已更新数据丢失
- 原因:多个事务同时处理数据,不行!
例子
死锁
场景:互相占用对方后续要用的资源,都在等待对方解锁释放。
解决:
- 超时回滚:innodb lock wait timeout,等待时间超过设置的阈值,让事务a回滚,另事务b就能继续。一般谁锁的数据范围大回滚谁。有弊端,事务周期长,有误杀嫌疑。
- 死锁检测:wait-for graph,采用等待图的方式来进行死锁检测
锁升级
InnoDB存储引|擎不存在锁升级的问题。
因为底层采用位图方式,是锁页面,不是每行加锁,所以锁单行还是多行开销一样
隔离级别
——————基于锁 + MVCC机制共同实现
-
READ UNCOMMITTED(读取未提交) : 最低的隔离级别,未解决脏读、幻读、不可重复读
-
READ COMMITTED(读取已提交) : 解决了脏读
- InnoDB引擎中——“严谨”
- 采用记录锁/Record Lock算法,更新时加X锁锁行,其他事务不能读,解决了脏读问题
- 采用MVCC,总是读取被锁定行最新的一份快照数据
-
REPEATABLE READ(可重复读) : 解决了脏读和不可重复读,很大程度降低幻读
- 是默认的隔离级别
- 采用临键锁/Next-Key Lock算法,更新时锁行+范围,解决了脏读、不可重复读,大部分幻读
- 采用MVCC,总是读取事务开始时的行数据版本
- Next-Key Lock锁,更新时锁行+范围,其他事务不能改,可以读,但读的是历史版本数据解决不可重复读,同理,只能读历史版本数据,很大程度解决幻读
- 当前读/改时读
- 快照读/普通读
-
SERIALIZABLE(可串行化) : 最高的隔离级别,解决了脏读、幻读、不可重复读。
- 读的时候就加Record Lock锁,S锁,(SELECT ... LOCK IN SHARE MODE)
隔离级别基于锁 + MVCC机制共同实现
一致性
undo log
持久性
redo log