我有一种"票彩票发行"问题。来到我这里的每个用户都需要从可用代码表中获取唯一代码。每个用户必须只获得一个代码,每个代码必须只给一个用户。将提供给用户的代码是表格中可用的第一个代码,该代码未被标记为"使用"。
此问题非常类似于:mysql - Locking rows for select query?
但差别很大:到目前为止,我已经测量了最多18个用户/秒的访问次数,因此我需要每个用户只锁定一行(我为每个用户工作的那一行)。锁定整个表可能是一个问题。
我已经读过" READ COMMITTED"可能有用:https://www.percona.com/blog/2012/08/28/differences-between-read-committed-and-repeatable-read-transaction-isolation-levels/
但这是我第一次做这样的事情,而且我有点迷失了如何准确地进行测试,以便稍后在将代码投入生产之前模拟这个巨大的负载。
答案 0 :(得分:3)
您不需要锁定行。
在处理唯一性时,您会设置UNIQUE
个约束。 始终即可。没有例外,永远。
关键不是性能而是数据完整性。这就是数据库的用途 - 包含VALID数据,以便您可以创建投影,计划等。
快速做到并不困难。 不要因性能而牺牲数据完整性。
免责声明:我没有在这里测试任何SQL。我发布的所有内容都是一个教育范例。它可能会复制粘贴它,我不保证这里显示的任何SQL的语法正确性。
每个用户必须只获得一个代码,并且必须提供每个代码 一个且只有一个用户
这定义了唯一性。有一个代码。一个用户只能有一个代码。让我们设计模型。
alter
用户表来使用此方法UNIQUE
约束处理)使用这种方法:
为什么我们会出错?因为我们会遇到唯一的约束,数据库只会拒绝输入。但这就是我们想要的东西以及我们所得到的东西。锁定行不是这些类型问题的解决方案。它变慢了。您需要在某个时刻解锁行。它容易出现并发问题。数据库处理唯一性的次数比您或任何其他程序员所能做的更好。因此,我们将使用数据库的机制。
代码表将包含代码。你没有定义 代码的样子,所以我假设它将成为一个字符串。我会选择varchar(255)
,因为我喜欢它。由于代码必须是唯一的,我将帮助自己一点点触发和一个独特的约束。我会使用sha1
哈希代码的值,将其保存到binary(20)
并使binary
列唯一。
我从中得到了什么:
- 我的代码现在看起来像任何东西,是任何类型角色的组合
- 我总是有固定长度的索引,所以我可以放置unique
约束而不用担心
- 我可以使用哈希搜索代码,但我可以向客户显示更友好的版本
- 由于unique
约束
CREATE TABLE codes (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
code_value VARCHAR(255) NOT NULL, -- this is the "friendly" value that customers get
code_hash binary(16) default null, -- this is the hash of the above value
is_used tinyint not null default '0', -- a small helper field when querying for which codes are "free"
primary key(id),
unique(code_hash)
) ENGINE = InnoDB;
处理散列的触发器,因此我们不必提供值:
DELIMITER $$
CREATE
TRIGGER `codes_before_insert` BEFORE INSERT
ON `codes`
FOR EACH ROW BEGIN
SET NEW.code_hash = UNHEX(SHA1(NEW.code_value));
END$$
DELIMITER ;
表格:
CREATE TABLE user2code (
id int unsigned not null auto_increment,
user_id int unsigned not null,
code_id int unsigned not null,
unique(code_id), -- this part allows for only 1 code to be used, ever
foreign key(user_id) references users(id) on delete cascade,
foreign key(code_id) references codes(id) on delete cascade
) ENGINE = InnoDB;
我们要在联络表上放置一个实用程序触发器。使用代码时,我们会更新codes
表并将is_used
设置为1
。这样做是为了更容易在自由/占用代码之间导航。我们可以通过JOIN
将user2code
表放到codes
表上来完成此操作,但我们想要更高效。
DELIMITER $$
CREATE
TRIGGER `user2code_after_insert` AFTER INSERT
ON `user2code`
FOR EACH ROW BEGIN
UPDATE `codes` SET is_used = 1 WHERE id = NEW.code_id;
END$$
DELIMITER ;
INSERT INTO user2code
(user_id, code_id)
VALUES
(1, (SELECT id FROM codes WHERE is_used = 0 LIMIT 1));
成果:
关键是要知道失败的交易不是。这个数据库的方式告诉我们嘿,你不能这样做,这就是为什么。
放置锁是危险的 - 它不能保证唯一性。只有唯一的限制。
祝你好运!答案 1 :(得分:2)
由于您只需要表中的一个字段,也许您可以尝试比引用的问题建议的更简单的事情。你真的不需要一组语句,你需要在一个表上进行更新,这个表能够从它更新的行返回一个值。所以,这实际上可以起作用:
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) ,最后对于第3点,您还需要存储用户已获得代码的信息,但这取决于您的用户的识别方式。例如。它可以像
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
列索引的说明)由于评论中提出了关于used
的索引的问题,我已经进行了快速的非正式基准测试。它基于上述解决方案,但也可能值得考虑使用类似结构和DML的任何其他解决方案。
免责声明:
--innodb-buffer-pool-size=4G --innodb-flush-log-at-trx-commit=2
,所有其他选项均为服务器默认值; mysqlslap
来自同一个软件包创建了四个表。除了引擎(MyISAM
vs InnoDB
)和used
列上的索引(索引与无索引)之外,结构是相同的。
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行相同的数据(不相似但实际上相同)。
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
仍然只执行一次键读取和一次读取:
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
当然,使用额外索引更新表比没有索引更新表要慢很多倍。我认为评论中的混淆来自于此。
评论中提出的另一个异议是使用自然主键,而不是代理主键,关注的是它会影响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
时,测试开始; 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
在这里更优越,它是&#39很可能通过适当调整服务器并使用更好的环境,它会发生变化。但我们可以看到,对于此工作流程使用自然PK
vs代理PK
时,性能不会下降。所以,这是个人偏好的问题。