我正在使用Laravel和Migrations来构建我的整个数据库结构。
问题描述
在架构中,我有一个pack
表,它属于user
和group
,需要为这些表的每个不同组合保留一种唯一的“索引”。
表示:基于不同的user_id
和group_id
递增的序号。例如:
| id | user_id | group_id | sequence |
| 1 | 1 | 1 | 1 |
| 2 | 1 | 2 | 1 |
| 3 | 1 | 3 | 1 |
| 4 | 1 | 1 | 2 |
| 5 | 1 | 2 | 2 |
| 6 | 1 | 3 | 2 |
| 7 | 2 | 1 | 1 |
| 8 | 2 | 2 | 1 |
| 9 | 2 | 3 | 1 |
这将用于引用视图层上的包:
用户1,这是第1组的第1组。
用户1,这是第1组的第2组。
用户1,这是第2组的第1组。
我设计了我的迁移(在up
上),如:
Schema::create('pack', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('user');
$table->integer('group_id')->unsigned();
$table->foreign('group_id')->references('id')->on('group');
$table->integer('sequence')->unsigned();
});
并使用此业务逻辑填充模型层上的$pack->sequence
字段。
问题1: 从理论上讲,这应该被认为是在所述情景中使用的最佳策略?
问题2:
有一些模式/方法可用于填充数据库层上的sequence
字段吗?
答案 0 :(得分:1)
您似乎已经拥有自动增量列id
。 MySQL每个表不支持多个自动增量列。
通常,在允许对表进行并发插入时,您无法获得所描述的行为。原因是您必须读取某个用户/组对的最大序列值,然后在插入新行时插入下一个值。
但是这会创建一个竞争条件,因为其他一些并发会话可能会做同样的事情,它会潜入并在会话的读取和插入步骤之间插入一个包含下一个序列值的行。
解决方案是以一种方式使用锁来防止同一个user_id和group_id的并发插入。 InnoDB将使用间隙锁来帮助解决这个问题。
示例:
打开两个MySQL客户端。在第一个会话中,试试这个:
mysql> begin;
mysql> select max(sequence) from pack where user_id=1 and group_id=1 for update;
+---------------+
| max(sequence) |
+---------------+
| 2 |
+---------------+
FOR UPDATE
锁定检查的行,和它锁定“gap”,这是插入具有相同user_id和group_id的其他行的地方。
要证明这一点,请尝试第二次会议:
mysql> begin;
mysql> insert into pack set user_id=1, group_id=1, sequence=3;
它挂了。它不能进行插入,因为它与第一个会话仍然保持的间隙锁相冲突。已经避免了竞争条件。
现在在第一次会议中,完成工作。
mysql> insert into pack set user_id=1, group_id=1, sequence=3;
mysql> commit;
提交后请注意,会立即释放会话1的锁。第二个会话解析了其被阻止的INSERT,但它正确地收到错误:
ERROR 1062 (23000): Duplicate entry '1-1-3' for key 'user_id'
当然,会话2应该完成相同的SELECT ... FOR UPDATE。在它可以解决锁定冲突之前,它也会被阻止。一旦解决,它就会返回正确的新最大序列值。
锁只是每个user_id / group_id组合,当且仅当你有一个合适的索引时。我用过:
ALTER TABLE pack ADD UNIQUE KEY (user_id, group_id, sequence);
获得该密钥后,SELECT ... FOR UPDATE可以在锁定它们时特定于正确的行集。
这意味着即使user_id = 1,group_id = 1被锁定,您仍然可以为user_id或group_id的任何其他值插入新条目。它们锁定索引的不同部分,因此没有冲突。
我鼓励你自己做一些实验,向自己证明你了解它是如何运作的。您无需编写任何PHP代码即可完成此操作。我刚刚打开了两个终端窗口,运行了mysql命令行客户端,并开始在mysql>
提示符处写入。你也可以!