数据库锁记录
乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据;
悲观锁就刚好相反,觉得自己读数据库的时候,别人可能刚好在写自己刚读的数据,其实就是持一种比较保守的态度;时间戳就是不加锁,通过时间戳来控制并发出现的问题。
共享锁,又称为读锁,可以查看但无法修改和删除的一种数据锁。
共享锁(S锁)又称为读锁,若事务T对数据对象A加上S锁(共享锁),则事务T只能读A;其他事务只能再对A加S锁(共享锁),而不能加X锁(排他锁),直到T释放A上的S锁(共享锁)。这就保证了其他事务可以读A,但在T释放A上的S锁(共享锁)之前不能对A做任何修改。
时间戳
从数据库行记录里取出一个字段timestamp为$timestamp,执行业务逻辑,存储数据时,如果$newTimestamp=$timestamp+1与数据库字段timestamp值对比,如果$newTimestamp大于数据库字段timestamp值的话,则执行写操作;等于或者小于的话,不执行写操作。
public function lockTest(){ $balance = Db::name('balance')->where('id',1)->find(); $money = $balance['money'] + 3; $timestamp = $balance['timestamp']; $newTimestamp = $timestamp + 1; sleep(20); $res = Db::name('balance')->where(array('id'=>1,'timestamp'=>array('<',$newTimestamp)))->update(array('timestamp'=>$newTimestamp,'money'=>$money)); if($res){ echo '更新成功'; }else{ echo '更新失败'; } } |
文件锁
<?php namespace app\common\controller; use think\Controller; /** * Block锁类 */ class Block extends Controller{ private $fps = []; /** * 加锁 */ public function file_lock($filename){ $fp_key = sha1($filename); //echo '锁文件'; $filename = './lock/'.$filename.'.txt'; $this->fps[$fp_key] = fopen($filename, 'w+'); if($this->fps[$fp_key]){ return flock($this->fps[$fp_key], LOCK_EX); } return false; } /** * 解锁 */ public function file_unlock($filename){ $fp_key = sha1($filename); //$filename = './lock/'.$filename.'.txt'; if($this->fps[$fp_key] ){ flock($this->fps[$fp_key] , LOCK_UN); fclose($this->fps[$fp_key] ); } } } |
当执行事务时,相当于执行了锁,来保持数据的一致性,但是锁分多种,有行锁,表锁。行锁就是只锁定那一行,那一条记录,别的连接下的操作还可以操作这张表。表锁就是锁定整张表,只有当前连接执行完事务,才可以解锁。
就效率而然,当然是行锁好,适用与多线程和高并发的情况,不过行锁对数据库会带来额外的开销。表锁高并发就差一点了,但单个的话快一点。
以mysql为例,有索引并且使用了该索引当条件的时候就是行锁,没有索引的时候就是表锁。innodb 的行锁是在有索引的情况下,没有索引的表是锁定全表的.
锁是对于别的连接来说,不是对于当前连接,即当前连可以一直不加rollback,commit,一路更新,但是别的连接就不行,必须等加了锁的连接释放(rollback,commit)后才能更新,插入
建议:对于插入操作,一般加表锁,但是对于修改和删除操作,最好加行锁,这样在高并发时,不用等太久
加锁方式:
select * from testlock where id=1 for update;——查询加锁,查询时不允许更改,该语句在自动提交为off或事务中生效,相当于更改操作,模拟加锁
update testlock name=name;——列=同一个列
更新操作,插入,删除操作,在事务中均属于加锁
以上转载自 https://blog.csdn.net/zyu67/article/details/46669889
下边为tp5事务中加锁验证
① 更新操作,在事务中均属于加锁
public function test2() { Db::startTrans(); try{ Db::name('test2')->where('id',1)->update(array('test'=>100)); sleep(10); Db::commit(); echo date('H:i:s'); echo '执行完毕'; }catch(\Exception $e){ Db::rollback(); } } public function test3(){ Db::name('test2')->where('id',1)->update(array('test'=>20)); sleep(10); echo date('H:i:s'); echo '执行完毕'; } public function test4(){ Db::name('test2')->where('id',2)->update(array('test'=>20)); echo date('H:i:s'); echo '执行完毕'; } |
test2 16:31:38执行完毕
test3 16:31:48执行完毕 更新未提交需要等待
test4 16:31:16执行完毕 因为采用行锁,所以对其他主键没有影响
② 查询加行锁 防止脏数据
public function test2() { Db::startTrans(); try{ Db::name('test2')->lock(true)->where('id',1)->find(); sleep(10); Db::commit(); echo date('H:i:s'); echo '执行完毕'; }catch(\Exception $e){ Db::rollback(); } } public function test3(){ Db::name('test2')->where('id',1)->find(); echo date('H:i:s'); echo '执行完毕'; } public function test4(){ Db::name('test2')->where('id',1)->update(array('test'=>20)); echo date('H:i:s'); echo '执行完毕'; } |
16:43:21执行完毕
16:43:21执行完毕 可以执行查询操作,如果test1事务中执行更新操作,会造成test2数据脏读
16:43:21执行完毕 需等待锁资源释放才能执行更新操作
③ 查询加行锁 需等待锁资源释放,下一个请求才能继续加锁
public function test2() { Db::startTrans(); try{ $res = Db::name('test2')->lock(true)->where('id',1)->find(); Db::name('test2')->where('id',1)->update(array('test'=>117)); sleep(10); Db::commit(); echo date('H:i:s'); echo '执行完毕'; }catch(\Exception $e){ Db::rollback(); } } public function test4(){ $res = Db::name('test2')->lock(true)->where('id',1)->find(); dump($res); echo date('H:i:s'); echo '执行完毕'; } |
test2 22:51:00执行完毕
test3
array(2) {
[“id”] => int(1)
[“test”] => string(3) “117”
}
22:51:00执行完毕
④ 事务中更新数据,不会阻塞其他数据读
public function test2() { Db::startTrans(); try{ Db::name('test2')->where('id',1)->update(array('test'=>3)); sleep(10); Db::commit(); echo date('H:i:s'); echo '执行完毕'; }catch(\Exception $e){ Db::rollback(); } } public function test4(){ $res = Db::name('test2')->where('id',1)->find(); dump($res); echo date('H:i:s'); echo '执行完毕'; } |
test2 23:37:41执行完毕
test4 array(2) {
[“id”] => int(1)
[“test”] => string(1) “1”
}
23:37:32执行完毕
5.27 比较好的一个加锁方法
Db::startTrans(); try { /***①减少用户百合***/ Db::name('user')->where('id', $userId)->setDec('baihe', $orderInfo['count']); /***②减少用户百合写入日志***/ baiheLog($userId, $userInfo['baihe'], -$orderInfo['count'], $userInfo['baihe'] - $orderInfo['count'], 2, '用户在交易大厅出售,减少百合数量', 1); /***③更新百合订单交易状态***/ $res = Db::name('lolli_order') //事务中执行写操作,会加上锁 ->where('id', $orderId) ->where('status',0) ->update($orderInfo); if($res){ Db::commit(); }else{ throw new \Exception('交易失败,请重新下单'); } } catch (\Exception $e) { // 捕获异常回滚事务 Db::rollback(); $this->error('交易失败,请重新下单'); } |
一个简单的方法,防止库存超发
创建测试表
CREATE TABLE `test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `number` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='测试表'; |
注意:number字段设置为 unsigned,如果number存在超发的情况,事物就会回滚
public function test(){ Db::startTrans(); try { $res = Db::table('test')->where('id', 3)->setDec('number'); //抛出异常回滚 Db::table('test')->insert(array('number'=>7)); Db::commit(); } catch (\Exception $e) { Db::rollback(); } } |
一个简单的方法,防止并发添加,同样的记录 多个字段唯一索引
CREATE TABLE `test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `number` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '数量', `goods_id` int(11) NOT NULL DEFAULT '0' COMMENT '商品ID', PRIMARY KEY (`id`), UNIQUE KEY `number` (`number`,`goods_id`) ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='测试表'; |
public function test2(){ Db::startTrans(); try { Db::table('test')->insert(array('number'=>3,'goods_id'=>3)); Db::commit(); } catch (\Exception $e) { Db::rollback(); $this->error('发生了一个错误'); } } |