Mysql:只为选择

时间:2016-11-29 12:39:03

标签: mysql

我有一种"票彩票发行"问题。来到我这里的每个用户都需要从可用代码表中获取唯一代码。每个用户必须只获得一个代码,每个代码必须只给一个用户。将提供给用户的代码是表格中可用的第一个代码,该代码未被标记为"使用"。

此问题非常类似于: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/

但这是我第一次做这样的事情,而且我有点迷失了如何准确地进行测试,以便稍后在将代码投入生产之前模拟这个巨大的负载。

2 个答案:

答案 0 :(得分:3)

您不需要锁定行

在处理唯一性时,您会设置UNIQUE个约束。 始终即可。没有例外,永远。

关键不是性能而是数据完整性。这就是数据库的用途 - 包含VALID数据,以便您可以创建投影,计划等。

快速做到并不困难。 不要因性能而牺牲数据完整性

免责声明:我没有在这里测试任何SQL。我发布的所有内容都是一个教育范例。它可能会复制粘贴它,我不保证这里显示的任何SQL的语法正确性。

问题

  

每个用户必须只获得一个代码,并且必须提供每个代码   一个且只有一个用户

这定义了唯一性。有一个代码。一个用户只能有一个代码。让我们设计模型。

解决方案

  • 我们会将代码保存到一个表格中
  • 我们将使用联结表将用户与代码链接起来。使用了连接表,因此我们不需要alter用户表来使用此方法
  • 代码将具有唯一值(由UNIQUE约束处理)
  • 连接表每个用户只允许使用1个代码,但是如果以后需要,同一个用户仍然可以分配其他代码

使用这种方法:

  • 我们收到有关重复条目的错误
  • 我们处理这些错误

为什么我们会出错?因为我们会遇到唯一的约束,数据库只会拒绝输入。但这就是我们想要的东西以及我们所得到的东西。锁定行不是这些类型问题的解决方案。它变慢了。您需要在某个时刻解锁行。它容易出现并发问题。数据库处理唯一性的次数比您或任何其他程序员所能做的更好。因此,我们将使用数据库的机制。

代码表

代码表将包含代码。你没有定义 代码的样子,所以我假设它将成为一个字符串。我会选择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 ;

联结表

  1. 已使用,因此我们不必更改任何现有的与用户相关的表格
  2. 允许每个用户使用一个代码
  3. 允许一个用户使用多个代码
  4. 包含用于强制执行数据完整性的外键
  5. 表格:

    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。这样做是为了更容易在自由/占用代码之间导航。我们可以通过JOINuser2code表放到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. 如果出现错误,您可以通过重复交易来处理错误。您可以重复几次(3-4)次交易,如果失败,请让用户稍后再试。这与UX有关,我个人对此一无所知。
    3. 关键是要知道失败的交易不是。这个数据库的方式告诉我们嘿,你不能这样做,这就是为什么

      放置锁是危险的 - 它不能保证唯一性。只有唯一的限制。

      祝你好运!

答案 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;

它是原子的,因此您不需要事务或显式表锁定。是否制作表InnoDBMyISAM应根据您环境中的比较性能凭经验确定,因为它可能取决于许多超出范围的内容。

诚信说明

请注意,它只是一个存根,而不是一个完整的解决方案。实际上,您需要更多逻辑来确保您的所有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的任何其他解决方案。

免责声明:

  • 结果中的绝对值完全不相关,测试是在常规的Debian桌面安装上执行的,而不是以任何方式进行基准测试;
  • 结果并非旨在证明建议的解决方案是好的,只是为了检查所讨论的一些要点;
  • 服务器不是InnoDB调整的,可以通过配置正确的InnoDB表获得更好的性能,这只是一个非常粗略的比较。

测试设置

  • MySQL服务器5.6.34,来自官方网站的64位二进制tarball
  • 选项:--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行相同的数据(不相似但实际上相同)。

测试流程

  • 每张桌子都进行了两次测试。
  • 每个测试都运行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仍然只执行一次键读取和一次读取:

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

当然,使用额外索引更新表比没有索引更新表要慢很多倍。我认为评论中的混淆来自于此。

更新2 (使用自然PK vs代理PK的注意事项)

评论中提出的另一个异议是使用自然主键,而不是代理主键,关注的是它会影响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在这里更优越,它是&#39很可能通过适当调整服务器并使用更好的环境,它会发生变化。但我们可以看到,对于此工作流程使用自然PK vs代理PK 时,性能不会下降。所以,这是个人偏好的问题。