案例一、标准的事务配合lock处理并发的例子
1、存在以下2个方法(理论上是一个方法,为了模拟并发所以建立2个方法配合sleep阻塞实现假并发需求)
// 先运行此方法[8,9,11]id的原始age均为10,"10:26:15" public function testTrans(){ Db::startTrans(); try{ print_dump(date('H:i:s')); // "10:26:15" $age = Db::name('user')->lock(true)->where('id', 'in',[8,9])->column('age'); print_dump(date('H:i:s')); // "10:26:15" print_dump($age); // [10, 10] Db::name('user')->where('id',8)->setInc('age',10); $age2 = Db::name('user')->where('id',8)->value('age'); print_dump($age2); // 20 print_dump(date('H:i:s')); // "10:26:15" sleep(10); // 模拟其他的同步阻塞方法 print_dump(date('H:i:s')); // "10:26:25" Db::commit(); return date('H:i:s').'_lock_true_'.date('H:i:s'); } catch (\Exception $e) { Db::rollback(); throw $e; } return date('H:i:s').'_lock_false_'.date('H:i:s'); }
// 再运行此方法(隔了2秒后)[8,9,11]id的原始age均为10,"10:26:17" public function testTransSync(){ Db::startTrans(); try{ print_dump(date('H:i:s')); // "10:26:17" $age = Db::name('user')->lock(true)->where('id', 'in',[8,9])->column('age'); print_dump(date('H:i:s')); // "10:26:25" print_dump($age); // [20, 10] Db::name('user')->where('id',8)->setInc('age',10); $age2 = Db::name('user')->where('id',8)->value('age'); print_dump($age2); // 30 print_dump(date('H:i:s')); // "10:26:25" sleep(10); // 模拟其他的同步阻塞方法 print_dump(date('H:i:s')); // "10:26:35" Db::commit(); return date('H:i:s').'_lock_true_'.date('H:i:s'); } catch (\Exception $e) { Db::rollback(); throw $e; } return $time.'_lock_false_'.date('H:i:s'); }
2、测试时先运行方法1,再运行方法2(理论上一起运行模拟并发)
$a = $this->testTrans(); die;
$b = $this->testTransSync(); die;
3、观察运行结果(注意:[8,9,11]id的原始age均为10)
4、总结结论
1.方法1给[8,9]上锁,10秒后才更新age;方法2待方法1运行完,才能获取到[8,9]中的age值(已变成20),10秒后才能更改
2.lock默认是行级锁,注意不要把整个表都锁了,例如 where('id','gt',0)->lock(true)->select等就会锁整个表
3.lock锁是配合事务启用的,若不配合事务使用,lock方法并不会报错,但lock的效果并不生效;例如,把以上两方法的事务去掉(把try内的代码移出来但保留lock),跟没有lock的结果是一样的(虽然手动运行结果(由于模拟不了并发),但实际并发时,可能运行到方法1的查找age时修改前,方法2同时也运行到查找age时修改前,因此方法1和方法2查找到的age依然是原始值10而导致修改后的结果可能出错的bug)
4.lock锁在select,find,column等查找方法才能使用,并不是update,insert方法使用的
5.lock悲观锁上锁后,别的操作不能查不能改,lock乐观锁上锁后,别的操作可以查不能改(注意:别的操作指的可能是事务,也可能是非事务)
6.lock配合事务实质是保证整个事务操作中从上锁到修改的原子性,若单纯只有事务没有lock并不能保证整个事务操作中从上锁到修改的原子性,例如案例二
案例二、只有事务没有lock上锁并发时会出问题
1、若把上述中的2方法中的事务中的lock true去掉,再把setInc方法改为update,则
// 先运行此方法[8,9,11]id的原始age均为10,"10:26:15" public function testTrans(){ Db::startTrans(); try{ print_dump(date('H:i:s')); // "10:26:15" $age = Db::name('user')->where('id', 'in',[8,9])->column('age'); print_dump(date('H:i:s')); // "10:26:15" print_dump($age); // [10, 10] $updateArr = [ 'age' => $age[0] + 10, ]; Db::name('user')->where('id',8)->update($updateArr); $age2 = Db::name('user')->where('id',8)->value('age'); print_dump($age2); // 20 print_dump(date('H:i:s')); // "10:26:15" sleep(10); // 模拟其他的同步阻塞方法 print_dump(date('H:i:s')); // "10:26:25" Db::commit(); return date('H:i:s').'_lock_true_'.date('H:i:s'); } catch (\Exception $e) { Db::rollback(); throw $e; } return date('H:i:s').'_lock_false_'.date('H:i:s'); }
// 再运行此方法(隔了2秒后)[8,9,11]id的原始age均为10,"10:26:17" public function testTransSync(){ Db::startTrans(); try{ print_dump(date('H:i:s')); // "10:26:17" $age = Db::name('user')->where('id', 'in',[8,9])->column('age'); print_dump(date('H:i:s')); // "10:26:17" print_dump($age); // [10, 10] $updateArr = [ 'age' => $age[0] + 10, ]; Db::name('user')->where('id',8)->update($updateArr); $age2 = Db::name('user')->where('id',8)->value('age'); print_dump($age2); // 10 print_dump(date('H:i:s')); // "10:26:25" sleep(10); // 模拟其他的同步阻塞方法 print_dump(date('H:i:s')); // "10:26:35" Db::commit(); return date('H:i:s').'_lock_true_'.date('H:i:s'); } catch (\Exception $e) { Db::rollback(); throw $e; } return $time.'_lock_false_'.date('H:i:s'); }
2、观察结果&总结结论,方法2中获取到的值依然跟方法1中获取到的age值一样,是原始值10;方法2中update修改时间必须要等方法1中的事务commit后才修改(案例三中会详解),因此,并发时修改后age依然是20,并不是我们需要的正确结果30
案例三、事务中并发修改一个表的一行(例如id=8),需要等事务结束了才能执行下一个事务
1、依次执行方法1和方法2(纯事务没有lock上锁)
// 先运行此方法,"15:11:49" public function testTrans(){ Db::startTrans(); try{ print_dump(date('H:i:s')); // "15:11:49" Db::name('user')->where('id',8)->setInc('age',10); print_dump(date('H:i:s')); // "15:11:49" $age2 = Db::name('user')->where('id',8)->value('age'); print_dump($age2); // 20 print_dump(date('H:i:s')); // "15:11:49" sleep(10); // 模拟其他的同步阻塞方法 print_dump(date('H:i:s')); // "15:11:59" Db::commit(); return date('H:i:s').'_lock_true_'.date('H:i:s'); } catch (\Exception $e) { Db::rollback(); throw $e; } return date('H:i:s').'_lock_false_'.date('H:i:s'); }
// 再运行此方法(隔了3秒后),"15:11:52" public function testTransSync(){ Db::startTrans(); try{ print_dump(date('H:i:s')); // "15:11:52" Db::name('user')->where('id',8)->setInc('age',10); print_dump(date('H:i:s')); // "15:11:59" $age2 = Db::name('user')->where('id',8)->value('age'); print_dump($age2); // 30 print_dump(date('H:i:s')); // "15:11:59" sleep(10); // 模拟其他的同步阻塞方法 print_dump(date('H:i:s')); // // "15:12:09" Db::commit(); return date('H:i:s').'_lock_true_'.date('H:i:s'); } catch (\Exception $e) { Db::rollback(); throw $e; } return $time.'_lock_false_'.date('H:i:s'); }
2、观察结果&总结结论,事务2中的setInc方法需要等事务1执行完成后,才会执行。若把方法2改为非事务(即:try内的方法移到外面),依然是需要等事务1完成后才能修改方法2(方法2中的普通查找find并不需要等事务1完成后才能查找),即:事务中若修改行级表,会给此行添加乐观锁(可查此行不可改此行)
案例四、事务中并发修改一个表,一行(例如id=8),另一行(例如id=9),不需要等事务(id=8的)结束了才能执行下一个事务(id=9的),而是(id=9的)会马上执行
1、依次执行方法1和方法2(纯事务没有lock上锁)
// 先运行此方法,"15:11:49" public function testTrans(){ Db::startTrans(); try{ print_dump(date('H:i:s')); // "15:11:49" Db::name('user')->where('id',8)->setInc('age',10); print_dump(date('H:i:s')); // "15:11:49" $age2 = Db::name('user')->where('id',8)->value('age'); print_dump($age2); // 20 print_dump(date('H:i:s')); // "15:11:49" sleep(10); // 模拟其他的同步阻塞方法 print_dump(date('H:i:s')); // "15:11:59" Db::commit(); return date('H:i:s').'_lock_true_'.date('H:i:s'); } catch (\Exception $e) { Db::rollback(); throw $e; } return date('H:i:s').'_lock_false_'.date('H:i:s'); }
// 再运行此方法(隔了3秒后),"15:11:52" public function testTransSync(){ Db::startTrans(); try{ print_dump(date('H:i:s')); // "15:11:52" Db::name('user')->where('id',9)->setInc('age',10); print_dump(date('H:i:s')); // "15:11:52" $age2 = Db::name('user')->where('id',9)->value('age'); print_dump($age2); // 30 print_dump(date('H:i:s')); // "15:11:52" Db::commit(); return date('H:i:s').'_lock_true_'.date('H:i:s'); } catch (\Exception $e) { Db::rollback(); throw $e; } return $time.'_lock_false_'.date('H:i:s'); }