tp5.1事务中悲观锁使用

Mysql 发表时间:2021-11-12 11:10:33 作者:梁子亮 浏览次数:1065

案例一、标准的事务配合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');
}

上一篇   docker使用教程