这里有三 个概念:悲观锁、高并发、数据不一致
高并发:
高并发是指在同一个时间点,有很多用户同时的访问URL地址,比如:淘宝的双11,双12,就会产生高并发。
数据不一致:
一般导致数据库中数据不一致的根本原因有三种情况。
第一种情况:数据冗余
假如数据库中两个表都放了用户的地址,在用户的地址发生改变时,如果只更新了一个表的数据,那么两个表就有了不一致的数据。
第二种情况:并发控制不当
假如在飞机票订票系统中,如果两个购票点同时查询某张机票的订购情况,而且分别为订购了这张机票,如果并发控制不当,就会造成同一张机票卖给两个用户的情况。由于系统没有进行并发控制或者并发控制不当,造成数据不一致。
第三中情况:故障和错误
如果软硬件出现故障或者操作错误导致数据丢失或数据损坏,引起数据不一致。因此我们需要提供数据库维护和数据库数据恢复的一些措施。
悲观锁:
在关系数据库管理系统中,悲观并发控制(悲观锁,PCC)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作的每行数据应用了锁,那只有当这个事务锁释放,其他事务才能够执行与该锁冲突的操作。
悲观锁,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度(悲观),因此在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,一般依靠数据库提供的锁机制。
有关mysql的锁机制,可以参见下面的文档:
https://www.cnblogs.com/leedaily/p/8378779.html
案例
商品goods表,假设商品的id为1,购买数量为1,status为1表示上架中,2表示下架。现在用户购买此商品,在不是高并发的情况下处理逻辑是:
- 查找此商品的信息;
- 检查商品库存是否大于购买数量;
- 修改商品库存和销量;
- 订单表orders中添加一条记录
上面这种场景在高并发访问的情况下很可能会出现问题。如果商品库存是100个,高并发的情况下可能会有1000个同时访问,在到达第2步的时候,都会检测通过。这样会出现商品库存是-900个的情况。显然着不满足需求!!!
实施方案
数据表goods表结构
测试数据:
订单表orders表结构
测试数据
以下使用THINKPHP5.0编写代码,实现顾客购买的行为
在index模块中,定义一个buy控制器
<?php
namespace app\index\controller;
use think\Controller;
use think\Db;
class Buy extends Controller
{
/**
* thinkphp使用悲观锁。悲观锁需要配合事务一起使用
* 商品表。购买数量为1,先锁定该商品,不让其他操作减库存。
*/
public function lock(){
$num = 1;
$goods_id = 1;
Db::execute('set autocommit=0');
Db::startTrans();
//使用主键或者其他索引字段时为行级锁,否则为表级锁
$where['id'] = $goods_id;
$where['status'] = 1;
$goods_info = Db::name('goods')->lock(true)->where($where)->find();
if(empty($goods_info)){
return '商品不存在';
}
$total = $goods_info['total']; //商品库存
$sell = $goods_info['sell']; //商品销量
if($total<$num){
return '库存不足';
}
$data['total'] = $total-$num;
$data['sell'] = $sell+$num;
$res = Db::name('goods')->where(array('id'=>$goods_id))->update($data);
if($res){
$order_data['goods_id'] = $goods_id;
$order_data['num'] = $num;
$order_data['create_time'] = time();
$order_res = Db::name('orders')->insert($order_data);
if($order_res){
Db::commit();
}else{
Db::rollback();
}
}else{
Db::rollback();
}
return '执行完毕';
}
/**
* 不加锁的情况
* @return string
*/
public function unlock(){
$num = 1;
$goods_id = 1;
DB::execute('set autocommit=0');
Db::startTrans();
$where['id'] = $goods_id;
$where['status'] = 1;
$goods_info = Db::name('goods')->where($where)->find();
if(empty($goods_info)){
return '商品不存在';
}
$total = $goods_info['total']; //商品库存
$sell = $goods_info['sell']; //商品销量
if($total<$num){
return '库存不足';
}
$data['total'] = $total-$num;
$data['sell'] = $sell+$num;
$res = Db::name('goods')->where(array('id'=>$goods_id))->update($data);
if($res){
$order_data['goods_id'] = $goods_id;
$order_data['num'] = $num;
$order_data['create_time'] = time();
$order_res = Db::name('orders')->insert($order_data);
if($order_res){
Db::commit();
}else{
Db::rollback();
}
}else{
Db::rollback();
}
return '执行完毕';
}
}
在这个控制器中有两个方法,lock()方法使用了悲观锁处理数据,unlock()方法没有使用悲观锁。两个方法都使用了事务处理。
在thinkphp5.0中:Lock方法是用于数据库的锁机制,如果在查询或者执行操作的时候使用:lock(true);就会自动在生成的SQL语句最后加上 FOR UPDATE 或者 FOR UPDATE NOWAIT (Oracle数据库)。
订单表Orders中的数据
商品表goods中卖出了5件商品,订单表中有5条记录,符合数据的一致性。
在高并发的情况下,不使用悲观锁的unlock()方法就会出现数据不一致的情况。
下面借助apache下的ab.exe生成高并的请求
查看一下数据表
在同时有100人请求的情况 下,商品的库存减少了31个,但订单却增加了40个
你可以使用ab.exe 同时生成1000个请求,看看数据表goods和orders的数据是否一致。
接下来,我们测试使用悲观锁的数据处理
查看数据表的情况:
使用悲观锁的高并发测试结果:商品 goods表卖出50,订单orders中共有50条记录,数据保持了一致性。