我有一个名为migration_logs
的键值表,其中包含2列键,唯一且值为varchar(255)。
CREATE TABLE `migration_logs` (
`key` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`value` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
UNIQUE KEY `migration_logs_key_unique` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
然后我有以下Laravel代码:
class MigrationLog {
...
public static function beginMigration($userId)
{
$shouldMigrate = false;
$key = "luser:$userId/migration_pushed";
\DB::beginTransaction();
if (!self::query()->where('key', '=', $key)->first()) {
self::create([
'key' => $key,
'value' => Carbon::now(),
]);
$shouldMigrate = true;
}
\DB::commit();
return $shouldMigrate;
}
}
注意:您可能认为我没有适当地捕获异常,这是真的。但我的问题是关于MySQL的行为,而不是PHP代码中的异常处理。
你不需要知道laravel来帮助我。 beginMigration()所做的是检查密钥是否存在,如果没有,为mysql事务内部设置一些值。隔离级别是默认的MySql。
当两个进程使用相同的输入调用beginMigration()时,我希望一个设置密钥而另一个不执行任何操作。
但是,我发现一个进程设置密钥而另一个进程尝试设置密钥的情况,因此抛出了SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
异常。
我想知道怎么可能?怎么了?以及如何解决?
MySQL版本:5.6.27
答案 0 :(得分:0)
事务的隔离确保在数据库事务中只能看到提交的数据。
BEGIN
INSERT INTO mytable (a,b,c ) VALUES ('x', 'y', 'z' );
INSERT INTO mytable2 (d,e,f ) VALUES ('u', 'v', 'w' );
COMMIT
数据库的其他用户既不会看到' x'' y'' z'和' u'' v',' w'或两者。不是交易的一半。
在事务中查询时,您正在查询已提交的数据。所以插入相同数据的2个进程都会看到空间可用。
当数据库系统执行冲突提交时,它确保只有第一个事务成功,并且失败(通过抛出异常)所有其他事务。
从php
和laravel
文档中可以看出,commit
的预期行为是个例外。
显示的代码会将$shouldMigrate
设置为true,但如果失败,则会抛出异常。
如果$shouldMigrate
的范围超出了函数范围,则会导致失败。也可能有一个try
/ catch
块阻止进程中止。捕获的处理可能不会传播错误。
try {
\DB::commit();
} catch (Exception $e) {
echo 'Exception', $e->getMessage(), "\n";
$shouldMigrate = false;
}
这是我能想到的最简单的修复方法。
答案 1 :(得分:0)
听起来我应该在开始执行此查询之前提高隔离级别:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
由此,我从MySQL收到此异常,而不是完整性约束违例异常:
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction
话虽如此,对我的问题更好的解决方案是依赖于mysql错误而不是使用事务:
public static function beginMigration($userId)
{
$key = "luser:$userId/migration_pushed";
try {
self::create([
'key' => $key,
'value' => Carbon::now(),
]);
return true;
} catch (\Exception $e) {
return false;
}
}