我们有桌子
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.插入和删除不需要的行。
答案 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.拥抱死锁
您无法阻止所有类型的死锁,只能在它们发生时处理它们。并发系统应设计为预测一定数量的死锁,并在必要时重试操作。