由于您只需要表格中的一个字段,也许您可以尝试一些比参考问题建议的更简单的方法。您实际上并不需要一组语句,您需要对一个表进行更新,该表将能够从它更新的行中返回一个值。所以,这实际上可以工作:
CREATE TABLE codes (code CHAR(16) PRIMARY KEY, used BOOL DEFAULT 0, INDEX(used));
CREATE TRIGGER pick_code AFTER UPDATE ON codes FOR EACH ROW SET @your_code = OLD.code;
--populate the table
现在,连接中的每个用户都运行
UPDATE codes SET used = 1 WHERE used = 0 LIMIT 1;
然后这应该返回选择的代码:
SELECT @your_code;
它是原子的,因此您不需要为此执行事务或显式表锁定。是否制作表格InnoDB 和MyISAM 应根据您的环境中的比较性能凭经验决定,因为它可能取决于许多超出此处范围的因素。
完整性说明
请注意,这只是一个存根,而不是完整的解决方案。实际上,您需要更多逻辑来确保您的所有 4 个要求:
- 只有一个用户获得代码;
- 至少一个用户获得了一个代码;
- 只给一个用户一个代码;
- 至少向用户提供了一个代码。
存根解决了最后一点,第一个和第二个是崩溃安全问题(您应该能够确保使用适当的 InnoDB 设置,即使在其他方面 InnoDB 在此流程中将不如 MyISAM) ,最后对于第三点,您还需要存储用户已获得代码的信息,但这取决于您的用户是如何被识别的。例如。可能是这样的
CREATE TABLE codes (code CHAR(16) PRIMARY KEY, used BOOL DEFAULT 0, assignee VARCHAR(128), UNIQUE(assignee), INDEX(used));
CREATE TRIGGER pick_code BEFORE UPDATE ON codes FOR EACH ROW SET @your_code = OLD.code, NEW.assignee = CURRENT_USER();
(只是另一个存根——它可以以完全不同的方式完成)。
更新(关于为used 列建立索引的说明)
自从 cmets 提出关于 used 索引的问题以来,我已经运行了一个快速的非正式基准测试。它基于上述解决方案,但也值得考虑使用任何其他使用类似结构和 DML 的解决方案。
免责声明:
- 结果中的绝对值完全不相关,测试是在常规 Debian 桌面安装上执行的,没有针对基准测试进行任何调整;
- 结果并不是为了证明建议的解决方案是好的,只是为了检查讨论的一些要点;
- 服务器没有经过 InnoDB 调优,使用适当配置的 InnoDB 表可能会获得更好的性能,这只是一个非常粗略的比较。
测试设置
- MySQL server 5.6.34,官网64位二进制压缩包
- 选项:
--innodb-buffer-pool-size=4G --innodb-flush-log-at-trx-commit=2,所有其他选项都是服务器默认值;
- 客户端工具:
mysqlslap 来自同一个包
创建了四个表。除了引擎(MyISAM vs InnoDB)和used 列上的索引(索引 vs 无索引)之外,结构是相同的。
MySQL [test]> show create table codes_innodb \G
*************************** 1. row ***************************
Table: codes_innodb
Create Table: CREATE TABLE `codes_innodb` (
`code` char(17) NOT NULL,
`used` tinyint(1) DEFAULT '0',
PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
MySQL [test]> show create table codes_innodb_i \G
*************************** 1. row ***************************
Table: codes_innodb_i
Create Table: CREATE TABLE `codes_innodb_i` (
`code` char(17) NOT NULL,
`used` tinyint(1) DEFAULT '0',
PRIMARY KEY (`code`),
KEY `used` (`used`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
MySQL [test]> show create table codes_myisam \G
*************************** 1. row ***************************
Table: codes_myisam
Create Table: CREATE TABLE `codes_myisam` (
`code` char(17) NOT NULL,
`used` tinyint(1) DEFAULT '0',
PRIMARY KEY (`code`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
MySQL [test]> show create table codes_myisam_i \G
*************************** 1. row ***************************
Table: codes_myisam_i
Create Table: CREATE TABLE `codes_myisam_i` (
`code` char(17) NOT NULL DEFAULT '',
`used` tinyint(1) DEFAULT '0',
PRIMARY KEY (`code`),
KEY `used` (`used`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
1 row in set (0.01 sec)
每个表都填充了 50,000,000 行相同的数据(不相似,但实际上相同)。
测试流程
- 对每个表执行两个测试。
- 每个测试使用 20 个并发线程运行,所有线程都执行相同的更新,每个线程 250 次,总共 5000 次:
UPDATE codes_innodb SET used = 1 WHERE used = 0 LIMIT 1
- 当所有行的
used=0 时开始第一个测试(“初始”状态)。
- 当
used=1 1,005,000 行时开始第二次测试。
测试测量执行所有查询的总时间。
结果(以秒为单位)
| Table | Test 1 | Test 2 |
|--------------------|----------|----------|
| MyISAM with index | 0.459 | 0.333 |
| MyISAM, no index | 3.425 | 801.383 |
| InnoDB with index | 11.529 | 8.205 |
| InnoDB, no index | 19.646 | 2403.297 |
因此,在开始时,有或没有索引的结果是可比较的,即使有索引它们会更好一些。
但是,当我们必须深入研究数据时,结果会发生本质上的变化。使用索引,它们保持大致相同(忽略低值的波动),但没有索引,数据越深入,所需的时间就越长。
这是意料之中的,这就是原因。
有了索引,无论我们在哪里,UPDATE 仍然只执行一次 key 读取和一次 rnd 读取:
MySQL [test]> select used, count(*) from codes_myisam_i group by used;
+------+----------+
| used | count(*) |
+------+----------+
| 0 | 48990000 |
| 1 | 1010000 |
+------+----------+
2 rows in set (12.08 sec)
MySQL [test]> flush status;
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> update codes_myisam_i set used=1 where used=0 limit 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MySQL [test]> select * from information_schema.session_status where variable_name like 'Handler_read%' and variable_value > 0;
+------------------+----------------+
| VARIABLE_NAME | VARIABLE_VALUE |
+------------------+----------------+
| HANDLER_READ_KEY | 1 |
| HANDLER_READ_RND | 1 |
+------------------+----------------+
2 rows in set (0.00 sec)
但是没有索引,它执行的 rnd 读取次数与已更新的行数一样多:
MySQL [test]> select used, count(*) from codes_myisam group by used;
+------+----------+
| used | count(*) |
+------+----------+
| 0 | 48990000 |
| 1 | 1010000 |
+------+----------+
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> flush status;
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> update codes_myisam set used=1 where used=0 limit 1;
Query OK, 1 row affected (0.09 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MySQL [test]> select * from information_schema.session_status where variable_name like 'Handler_read%' and variable_value > 0;
+-----------------------+----------------+
| VARIABLE_NAME | VARIABLE_VALUE |
+-----------------------+----------------+
| HANDLER_READ_RND_NEXT | 1010001 |
+-----------------------+----------------+
1 row in set (0.00 sec)
当然,当我们执行大量单行更新并且每次都必须搜索一行时,这些结果是非常特定于这个特定流程的。因此,显然查找的惩罚超过了更新索引的惩罚。如果我们执行批量更新,情况会完全不同:
MySQL [test]> update codes_innodb set used = 1 where used = 0 limit 1000000;
Query OK, 1000000 rows affected (7.80 sec)
Rows matched: 1000000 Changed: 1000000 Warnings: 0
MySQL [test]> update codes_innodb_i set used = 1 where used = 0 limit 1000000;
Query OK, 1000000 rows affected (56.91 sec)
Rows matched: 1000000 Changed: 1000000 Warnings: 0
MySQL [test]> update codes_myisam set used = 1 where used = 0 limit 1000000;
Query OK, 1000000 rows affected (1.21 sec)
Rows matched: 1000000 Changed: 1000000 Warnings: 0
MySQL [test]> update codes_myisam_i set used = 1 where used = 0 limit 1000000;
Query OK, 1000000 rows affected (14.56 sec)
Rows matched: 1000000 Changed: 1000000 Warnings: 0
当然,使用额外索引更新表比不使用索引更新表要慢很多倍。我认为这就是 cmets 中的混乱的来源。
更新 2 (关于使用自然 PK 与替代 PK 的说明)
在 cmets 中提出的另一个反对意见是使用自然主键,而不是代理主键,担心它会影响 InnoDB 的性能。
这是一个类似的快速基准测试。
测试设置
与之前的测试相同的环境和服务器。两个InnoDB 表正在使用中。
第一个和之前一样,自然是PK:
Table: codes_innodb_i
Create Table: CREATE TABLE `codes_innodb_i` (
`code` char(17) NOT NULL,
`used` tinyint(1) DEFAULT '0',
PRIMARY KEY (`code`),
KEY `used` (`used`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
另一个带有代理 PK(并且在 code 上具有唯一索引,因为我们仍然希望确保它是唯一的——在第一个表中,PK 本身确保了这一点):
Table: codes_innodb
Create Table: CREATE TABLE `codes_innodb` (
`code` char(17) NOT NULL,
`used` tinyint(1) DEFAULT '0',
`pk` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`pk`),
UNIQUE KEY `code` (`code`),
KEY `used` (`used`)
) ENGINE=InnoDB AUTO_INCREMENT=50000001 DEFAULT CHARSET=latin1
每个表中有 50,000,000 行,相同的数据。
测试流程
- 当所有行的
used=0 时开始测试;
- 对每个表执行 10 次后续测试运行。
- 每次运行是 20 个并发线程,都执行相同的更新,每个线程 250 次,总共 5000 次:
UPDATE codes_innodb SET used = 1 WHERE used = 0 LIMIT 1
- 运行之间不更新表,即第一个以所有
used=0开头,第二个以表中的5000个used=1开头,以此类推
每个测试都会测量执行所有查询的总时间。
结果(以秒为单位)
| | Individual results | Avg |
|--------------|--------------------------------------------------------------|--------|
| natural PK | 8.061,6.782,5.712, 5.524,7.854,6.166,6.095,4.911,4.435,4.784 | 6.0324 |
| surrogate PK | 9.659,8.981,8.080,11.257,9.621,6.722,6.457,5.937,6.308,6.624 | 7.9646 |
尽管自然的PK 显示了更好的结果,但由于环境没有调整,我不会说自然的PK 在这里更胜一筹,但很可能通过适当的调整服务器并使用更好的环境它会改变。但我们可以看到,使用自然PK 与代理PK 相比,性能没有下降此工作流程。所以,这是个人喜好的问题。