在mysql

时间:2017-09-05 09:56:43

标签: mysql concurrency locking innodb

我们有桌子

CREATE TABLE TEST_SUBSCRIBERS (
  SUBSCRIPTION_ID varchar(255) NOT NULL COMMENT 'Subscriber id in format MSISDN-SERVICE_ID-TIMESTAMP',
  MSISDN varchar(12) NOT NULL COMMENT 'Subscriber phone',
  STATE enum ('ACTIVE', 'INACTIVE', 'UNSUBSCRIBED_SMS', 'UNSUBSCRIBED_PARTNER', 'UNSUBSCRIBED_ADMIN', 'UNSUBSCRIBED_REBILLING') NOT NULL,
  SERVICE_ID varchar(255) NOT NULL COMMENT 'Id of service',
  PRIMARY KEY (SUBSCRIPTION_ID)
)
ENGINE = INNODB
CHARACTER SET utf8
COLLATE utf8_general_ci;

在并行线程中,我们执行像这些

这样的动作(在java中)
1. Select active subscribers
SELECT  *
    FROM  TEST_SUBSCRIBERS
    WHERE  SERVICE_ID='web-sub-1'
      and  MSISDN='000000002'
      AND  STATE IN ('ACTIVE', 'INACTIVE');
2. If there are no such subscribers, I can insert it
INSERT INTO  TEST_SUBSCRIBERS
                    (SUBSCRIPTION_ID, MSISDN, STATE, SERVICE_ID)
             VALUES ('web-sub-1-000000002-1504624819', '000000002', 'ACTIVE', 'web-sub-1');

在并发模式下,2个线程可以尝试使用msisdn =" 000000002"插入行。和service-id =" web-sub-1"和不同的subscriptionId因为当前时间戳可以不同。两个线程执行第一次选择,得到零结果并且都插入。所以我们尝试将这两个查询连接到tranaction,但是对于不存在的行存在锁定问题 - 当我们需要锁定插入或类似的东西时。 我们不希望在这两个操作中锁定所有表,因为我们假设在这种情况下我们的系统工作得太慢。 我们无法为这种情况创建uniq密钥,因为对于一个abonent,可以有多个具有相同未订阅状态的行。如果我们尝试为同一服务插入2个订阅者,则主键可以包含具有不同秒数的时间戳。 我们尝试使用SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE,但是我们遇到了死锁,并且数据库服务器的操作很繁重。

对于测试,我们打开了2个终端并逐步完成:

# Window 1
mysql> start transaction;
mysql> SELECT SUBSCRIPTION_ID FROM TEST_SUBSCRIBERS s
           WHERE s.SERVICE_ID="web-sub-1" AND s.MSISDN="000000002" FOR UPDATE;

# Window 2
start transaction;
mysql> SELECT SUBSCRIPTION_ID FROM TEST_SUBSCRIBERS s
           WHERE s.SERVICE_ID="web-sub-1" AND s.MSISDN="000000002" FOR UPDATE;

# Window 1
mysql> INSERT INTO TEST_SUBSCRIBERS
            (SUBSCRIPTION_ID, MSISDN, STATE, SERVICE_ID) 
        VALUES('web-sub-1-000000002-1504624818', '000000002', 'ACTIVE', 'web-sub-1');

# Window 2
mysql> INSERT INTO TEST_SUBSCRIBERS
              (SUBSCRIPTION_ID, MSISDN, STATE, SERVICE_ID) 
           VALUES('web-sub-1-000000002-1504624819', '000000002', 'ACTIVE', 'web-sub-1');
ERROR 1213 (40001): Deadlock found when trying to get lock;
                    try restarting transaction

有没有办法在没有死锁且没有锁定全表的情况下这样做?我们分析的其他变体是: 单独的表 2.插入和删除不需要的行。

2 个答案:

答案 0 :(得分:0)

计划A.这将插入(如有必要)或无声地执行任何操作:

INSERT IGNORE ...;

计划B.这可能有点矫枉过正,因为没有什么需要“更新”:

INSERT INTO ...
    (...)
    ON DUPLICATE KEY UPDATE
    ...;

计划C.此声明大部分由IODKU取代:

REPLACE ... (same syntax as INSERT, but it does a silent DELETE first)

A和B(可能是C)是“原子”的,因此不存在死锁的可能性。

答案 1 :(得分:0)

回答@RickJames的回答。

计划D.使用READ-COMMITTED

窗口1

mysql> set tx_isolation='READ-COMMITTED';
mysql> start transaction;
mysql> SELECT SUBSCRIPTION_ID FROM TEST_SUBSCRIBERS s
       WHERE s.SERVICE_ID="web-sub-1" AND s.MSISDN="000000002" FOR UPDATE;

Window 2

mysql> set tx_isolation='READ-COMMITTED';
mysql> start transaction;
mysql> SELECT SUBSCRIPTION_ID FROM TEST_SUBSCRIBERS s
       WHERE s.SERVICE_ID="web-sub-1" AND s.MSISDN="000000002" FOR UPDATE;

窗口1

mysql> INSERT INTO TEST_SUBSCRIBERS (SUBSCRIPTION_ID, MSISDN, STATE, SERVICE_ID) 
    VALUES('web-sub-1-000000002-10', '000000002', 'ACTIVE', 'web-sub-1');

Window 2

mysql> INSERT INTO TEST_SUBSCRIBERS (SUBSCRIPTION_ID, MSISDN, STATE, SERVICE_ID) 
    VALUES('web-sub-1-000000002-10', '000000002', 'ACTIVE', 'web-sub-1');

<begins lock wait>

窗口1

mysql> commit;

Window 2

<lock wait ends immediately>

ERROR 1062 (23000): Duplicate entry 'web-sub-1-000000002-10' for key 'PRIMARY'

重复键错误不是死锁,但它仍然是一个错误。但它不会回滚整个事务,它只是取消了尝试插入。您仍然有一个活动事务,其中任何其他已成功执行的更改仍处于待处理状态。

计划E.使用队列

不要让并发Java线程插入数据库,只需让Java线程将项目输入消息队列(例如ActiveMQ)。然后创建一个Java线程,除了从队列中提取项目并将它们插入数据库之外什么都不做。这可以防止死锁,因为只有一个线程插入数据库。

计划F.拥抱死锁

您无法阻止所有类型的死锁,只能在它们发生时处理它们。并发系统应设计为预测一定数量的死锁,并在必要时重试操作。