MySQL鎖初探

本文的示例代碼參考laravel-lock

目錄

MySQL

docker run --name laravel-lock -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:5.7.17

docker exec -i laravel-lock mysql -uroot -psecret  <<< "CREATE DATABASE IF NOT EXISTS homestead DEFAULT CHARSET utf8 COLLATE utf8_general_ci;"

Project

Composer

composer create-project laravel/laravel laravel-lock --prefer-dist "5.5.*" && cd laravel-lock

sed -i "" "s/DB_USERNAME=homestead/DB_USERNAME=root/g" .env

Migration

php artisan make:model Models/Machine -m

vim database/migrations/*_create_machines_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateMachinesTable extends Migration
{
    public function up()
    {
        Schema::create('machines', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name')->index()->comment('機械名稱');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('machines');
    }
}
php artisan migrate

Seeder

php artisan make:seed MachinesTableSeeder

vim database/seeds/MachinesTableSeeder.php
<?php

use App\Models\Machine;
use Illuminate\Database\Seeder;

class MachinesTableSeeder extends Seeder
{
    public function run()
    {
        $machine = new Machine();
        $machine->name = 'machine1';
        $machine->save();

        $machine = new Machine();
        $machine->name = 'machine2';
        $machine->save();
    }
}
vim database/seeds/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        // $this->call(UsersTableSeeder::class);
        $this->call(MachinesTableSeeder::class);
    }
}
php artisan db:seed

Pessimistic Lock

Pessimistic Lock is when you lock the record for your exclusive use until you have finished with it

Lock In Share Mode

# MySQL connection 1
mysql root@localhost:(none)> USE homestead;
You are now connected to database "homestead" as user "root"
Time: 0.012s
mysql root@localhost:homestead> BEGIN;
Query OK, 0 rows affected
Time: 0.001s
mysql root@localhost:homestead> SELECT * FROM machines WHERE id = 1 LOCK IN SHARE MODE;
+----+----------+---------------------+---------------------+
| id | name     | created_at          | updated_at          |
+----+----------+---------------------+---------------------+
| 1  | machine1 | 2018-10-11 12:20:58 | 2018-10-11 12:20:58 |
+----+----------+---------------------+---------------------+
1 row in set
Time: 0.027s
# MySQL connection 2
mysql root@localhost:(none)> USE homestead;
You are now connected to database "homestead" as user "root"
Time: 0.007s
mysql root@localhost:homestead> SELECT * FROM machines WHERE id = 1;
+----+----------+---------------------+---------------------+
| id | name     | created_at          | updated_at          |
+----+----------+---------------------+---------------------+
| 1  | machine1 | 2018-10-11 12:20:58 | 2018-10-11 12:20:58 |
+----+----------+---------------------+---------------------+
1 row in set
Time: 0.018s
mysql root@localhost:homestead> UPDATE machines SET name='machine22' WHERE id=2;
Query OK, 1 row affected
Time: 0.046s
mysql root@localhost:homestead> SELECT * FROM machines;
+----+-----------+---------------------+---------------------+
| id | name      | created_at          | updated_at          |
+----+-----------+---------------------+---------------------+
| 1  | machine1  | 2018-10-11 12:20:58 | 2018-10-11 12:20:58 |
| 2  | machine22 | 2018-10-11 12:20:59 | 2018-10-11 12:20:59 |
+----+-----------+---------------------+---------------------+
2 rows in set
Time: 0.023s
mysql root@localhost:homestead> UPDATE machines SET name='machine11' WHERE id=1;
# pending
# MySQL connection 1
mysql root@localhost:homestead> COMMIT;
Query OK, 0 rows affected
Time: 0.005s
# MySQL connection 2
mysql root@localhost:homestead> UPDATE machines SET name='machine11' WHERE id=1;
Query OK, 1 row affected
Time: 11.423s
TRUNCATE machines;

php artisan db:seed

LOCK IN SHARE MODE locks only for write

For Update

# MySQL connection 1
mysql root@localhost:(none)> USE homestead;
You are now connected to database "homestead" as user "root"
Time: 0.005s
mysql root@localhost:homestead> BEGIN;
Query OK, 0 rows affected
Time: 0.005s
mysql root@localhost:homestead> SELECT * FROM machines FOR UPDATE;
+----+----------+---------------------+---------------------+
| id | name     | created_at          | updated_at          |
+----+----------+---------------------+---------------------+
| 1  | machine1 | 2018-10-10 07:22:53 | 2018-10-10 07:22:53 |
| 2  | machine2 | 2018-10-10 07:22:53 | 2018-10-10 07:22:53 |
+----+----------+---------------------+---------------------+
2 rows in set
Time: 0.015s
# MySQL connection 2
mysql root@localhost:(none)> USE homestead;
You are now connected to database "homestead" as user "root"
Time: 0.007s
mysql root@localhost:homestead> SELECT * FROM machines FOR UPDATE;
# pending
# MySQL connection 1
mysql root@localhost:homestead> COMMIT;
Query OK, 0 rows affected
Time: 0.001s
# MySQL connection 2
mysql root@localhost:homestead> SELECT * FROM machines FOR UPDATE;
+----+----------+---------------------+---------------------+
| id | name     | created_at          | updated_at          |
+----+----------+---------------------+---------------------+
| 1  | machine1 | 2018-10-10 07:22:53 | 2018-10-10 07:22:53 |
| 2  | machine2 | 2018-10-10 07:22:53 | 2018-10-10 07:22:53 |
+----+----------+---------------------+---------------------+
2 rows in set
Time: 38.915s

FOR UPDATE also prevents them from being selected

Table Lock vs Record Lock

# MySQL connection 1
mysql root@localhost:(none)> USE homestead;
You are now connected to database "homestead" as user "root"
Time: 0.012s
mysql root@localhost:homestead> BEGIN;
Query OK, 0 rows affected
Time: 0.001s
mysql root@localhost:homestead> SELECT * FROM machines LOCK IN SHARE MODE;
+----+----------+---------------------+---------------------+
| id | name     | created_at          | updated_at          |
+----+----------+---------------------+---------------------+
| 1  | machine1 | 2018-10-11 12:53:19 | 2018-10-11 12:53:19 |
| 2  | machine2 | 2018-10-11 12:53:19 | 2018-10-11 12:53:19 |
+----+----------+---------------------+---------------------+
2 rows in set
Time: 0.024s
# MySQL connection 2
mysql root@localhost:(none)> USE homestead;
You are now connected to database "homestead" as user "root"
Time: 0.007s
mysql root@localhost:homestead> UPDATE machines SET name='machine11' WHERE id=1;
# pending
mysql root@localhost:homestead> UPDATE machines SET name='machine22' WHERE id=2;
# pending

行級鎖都是基於索引的 如果一條SQL語句用不到索引是不會使用行級鎖的 此時會使用表級鎖把整張表鎖住

Optimistic Lock

Optimistic Lock is a strategy where you read a record, take note of a version number (other methods to do this involve dates, timestamps or checksums/hashes) and check that the version hasn't changed before you write the record back

Composer

composer require reshadman/laravel-optimistic-locking

Migration

php artisan make:migration update_machines_table_add_lock_version

vim database/migrations/*_update_machines_table_add_lock_version.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class UpdateMachinesTableAddLockVersion extends Migration
{
    public function up()
    {
        Schema::table('machines', function (Blueprint $table) {
            $table->integer('lock_version')->unsigned()->nullable();
        });
    }

    public function down()
    {
        Schema::table('machines', function (Blueprint $table) {
            $table->dropColumn('lock_version');
        });
    }
}
vim app/Models/Machine.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Reshadman\OptimisticLocking\OptimisticLocking;

class Machine extends Model
{
    use OptimisticLocking;

    protected $fillable = ['name'];
}

Unit Test

php artisan make:test MachineTest --unit
vim tests/Unit/MachineTest.php
<?php

namespace Tests\Unit;

use App\Models\Machine;
use Reshadman\OptimisticLocking\StaleModelLockingException;
use Tests\TestCase;

class MachineTest extends TestCase
{
    public function test_throws_exception_on_concurrent_change()
    {
        $truth = Machine::create([
            'name' => 'machine3',
        ]);

        $first = Machine::find($truth->id);
        $second = Machine::find($truth->id);

        $this->expectException(StaleModelLockingException::class);

        $first->name = $wantedTitle = 'machine31';
        $this->assertTrue($first->save());

        try {
            $second->name = 'machine32';
            $second->save();
        } catch (StaleModelLockingException $e) {
            $fetchedAfterFirstUpdate = Machine::find($truth->id);
            $this->assertEquals($fetchedAfterFirstUpdate->name, $wantedTitle);
            $this->assertEquals($fetchedAfterFirstUpdate->lock_version, $first->lock_version);
            $this->assertEquals($fetchedAfterFirstUpdate->lock_version, $truth->lock_version + 1);
            throw $e;
        }
    }
}
brew install phpunit

phpunit
PHPUnit 7.4.0 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 252 ms, Memory: 18.00MB

OK (3 tests, 7 assertions)
# MySQL connection
mysql root@localhost:(none)> USE homestead;
You are now connected to database "homestead" as user "root"
Time: 0.007s
mysql root@localhost:homestead> SELECT * FROM machines;
+----+-----------+---------------------+---------------------+--------------+
| id | name      | created_at          | updated_at          | lock_version |
+----+-----------+---------------------+---------------------+--------------+
| 1  | machine1  | 2018-10-10 07:22:53 | 2018-10-10 07:22:53 | <null>       |
| 2  | machine2  | 2018-10-10 07:22:53 | 2018-10-10 07:22:53 | <null>       |
| 3  | machine31 | 2018-10-12 01:36:20 | 2018-10-12 01:36:20 | 2            |
+----+-----------+---------------------+---------------------+--------------+
3 rows in set
Time: 0.013s

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章