数据库悲观锁
目录
几个核心关键点
autocommit
和 begin
、start transaction
的关系
- 作用范围不一样:
autocommit
是数据库innodb引擎级别的属性,相当于begin
、start transaction
是全局有效。 一旦使用SET AUTOCOMMIT=0
禁止自动提交,则在这个数据库内部的所有事务都不会自动提交,除非你手动的为每一个事务执行了commit
或者rollback
语句;而start transaction
和begin/commit
只能控制某一个事务。
- 优先级不一样:
start transaction
和begin/commit
优先级是高于autocommit
的。即: 即使autocommit=1
,如果语句显示声明开启事务 (begin/commit
),那么依旧需要自己手动提交或回滚。
排它锁和共享锁区别
- 共享锁
lock in share mode
:当上锁之后,另一个线程只可以读,不可以修改。 - 排它锁
select ... for update
:上锁之后,另一个线程不可以读和修改。
需要注意的是:若一个线程for update执行锁住某行数据,其他线程读取的时候,sql里没有for update,则可以正常读取。
【警告】
for update 的条件如果没有命中索引,则会锁表!!!
下面表是命中 不同类型索引 和 查询结果是否为空 的锁结果。
主键索引 | 普通索引 | 结果集是否为空 | 结果 |
---|---|---|---|
✅ | ❌ | ❌ | row lock |
❌ | ✅ | ❌ | row lock |
❌ | ❌ | ❌ | table lock |
使用范围
- 必须是
mysql
的innoDb
表。 - 必须开启
transaction
事务。
使用
共享锁和悲观锁代码示例
排它锁控制器
/**
* 使用悲观锁(排它锁)创建订单.
* @param LockRequest $request 请求验证器
* @return array ['code' => '200', 'msg' => 'ok', 'status' => true, 'data' => []]
*/
#[PostMapping(path: 'pessimism/for_update')]
#[Scene(scene: 'create_order')]
public function createOrderByForUpdate(LockRequest $request): array
{
$gid = intval($request->input('gid'));
$num = intval($request->input('number'));
$uid = $this->jwtPayload['data']['uid'];
$orderNo = $this->service->createOrderWithForUpdate($uid, $gid, $num);
return $this->result->setData(['oder_no' => $orderNo])->getResult();
}
排它锁逻辑
/**
* 悲观锁创建订单(排它锁).
* @param int $uid 用户id
* @param int $gid 商品id
* @param int $number 购买数量
* @return string 订单编号
*/
public function createOrderWithForUpdate(int $uid, int $gid, int $number = 1): string
{
// 开启事务
Db::beginTransaction();
try {
$orderNo = '';
/** 加上排它锁 @var Goods $goodInfo */
$goodInfo = Goods::query()->where(['id' => $gid])->lockForUpdate()->first();
// 商品不存在
if ($goodInfo === null) {
throw new BusinessException(...self::getErrorMap(ErrorCode::GOOD_NOT_FOUND));
}
// 库存不足
if ($goodInfo->stock < $number) {
throw new BusinessException(...self::getErrorMap(ErrorCode::GOOD_STOCK_EMPTY, [$goodInfo->name]));
}
// 创建订单
$orderNo = Math::getUniqueId();
(new Orders([
'uid' => $uid,
'gid' => $gid,
'order_no' => $orderNo,
'number' => $number,
'payment_money' => Math::mul($goodInfo->price, $number),
]))->save();
// 扣减库存
$goodInfo->stock = $goodInfo->stock - $number;
$goodInfo->save();
Db::commit();
} catch (Throwable $e) {
Db::rollBack();
}
if ($orderNo === '') {
throw new BusinessException(...self::getErrorMap(ErrorCode::STOCK_EMPTY));
}
return $orderNo;
}
共享锁控制器
/**
* 使用悲观锁(共享锁)创建订单.
* @param LockRequest $request 请求验证器
* @return array ['code' => '200', 'msg' => 'ok', 'status' => true, 'data' => []]
*/
#[PostMapping(path: 'pessimism/share_mode')]
#[Scene(scene: 'create_order')]
public function createOrderByShareMode(LockRequest $request): array
{
$gid = intval($request->input('gid'));
$num = intval($request->input('number'));
$uid = $this->jwtPayload['data']['uid'];
$orderNo = $this->service->createOrderWithShareMode($uid, $gid, $num);
return $this->result->setData(['oder_no' => $orderNo])->getResult();
}
共享锁逻辑
/**
* 悲观锁创建订单(共享锁).
* @param int $uid 用户id
* @param int $gid 商品id
* @param int $number 购买数量
* @return string 订单编号
*/
public function createOrderWithShareMode(int $uid, int $gid, int $number = 1): string
{
// 开启事务
Db::beginTransaction();
try {
/** 加上共享锁 @var Goods $goodInfo */
$goodInfo = Goods::query()->where(['id' => $gid])->sharedLock()->first();
// 商品不存在
if ($goodInfo === null) {
throw new BusinessException(...self::getErrorMap(ErrorCode::GOOD_NOT_FOUND));
}
// 库存不足
if ($goodInfo->stock < $number) {
throw new BusinessException(...self::getErrorMap(ErrorCode::GOOD_STOCK_EMPTY, [$goodInfo->name]));
}
// 创建订单
$orderNo = Math::getUniqueId();
(new Orders([
'uid' => $uid,
'gid' => $gid,
'order_no' => $orderNo,
'number' => $number,
'payment_money' => Math::mul($goodInfo->price, $number),
]))->save();
// 扣减库存
$goodInfo->stock = $goodInfo->stock - $number;
$goodInfo->save();
Db::commit();
} catch (Throwable $e) {
$orderNo = '';
Db::rollBack();
}
if ($orderNo === '') {
throw new BusinessException(...self::getErrorMap(ErrorCode::STOCK_EMPTY));
}
return $orderNo;
}
注意事项
【注意】
- 锁定的条件一定要注意,必须走索引,最好走主键索引,因为没有命中索引会锁表。
- 当 显式事务 锁住某行数据时,该行数据其他的 非显式
update
操作也会锁等待,因为update
单个SQL也是事务。不要以为其他update
没在BEGIN
COMMIT
中就不会等待。 - 最好不要使用
MySQL
进行显式锁操作。数据库应该更专注作为数据存储的存在。
查看事务和锁
查看事务和锁情况
【额外补充】
通过
information_schema.INNODB_TRX
表来查看事务和锁的情况。information_schema.INNODB_TRX
字段说明:
// 查看事务情况
select * from information_schema.innodb_trx
使用场景
- 并发较小 、 相对独立(尽可能锁住的行不被其他业务更新) 、一致性较强 的数据库操作。