ホームページ  >  記事  >  データベース  >  デッドロックとは何ですか? MySQL デッドロックについての理解を話しましょう

デッドロックとは何ですか? MySQL デッドロックについての理解を話しましょう

青灯夜游
青灯夜游転載
2022-08-24 10:12:502216ブラウズ

デッドロックとは何ですか?次の記事では、MySQL デッドロックについて説明します。 MySQL でデッドロックが発生するために必要な条件と、デッドロックの問題を解決する方法について話しましょう。お役に立てれば幸いです。

デッドロックとは何ですか? MySQL デッドロックについての理解を話しましょう

#1. デッドロックとは何ですか?

デッドロックとは、2 つ以上の異なるプロセスまたはスレッドにおいて、共通リソースの競合またはプロセス (またはスレッド) 間の通信により各スレッドがブロックされている状況を指します。外力がなければ、最終的にはシステム全体が崩壊してしまいます。

2. Mysql でのデッドロックの必要条件

  • ##リソース排他条件

# は、同じリソースをめぐって競合する場合の複数のトランザクションの相互排他性を指します。つまり、リソースは一定期間内に 1 つのトランザクションによってのみ占有されます。これは排他的リソース (行ロックなど) とも呼ばれます。

  • リクエスト アンド ホールド条件

ロック A はトランザクション a で取得されましたが、新しいロックが取得されたことを意味しますB が要求され、ロック B は別のトランザクション b によってすでに占有されています。このとき、トランザクション a はブロックされますが、取得したロック A は保持されます。

    #非剥奪条件
は、ロック A がトランザクション a で取得されており、その前にロックできないことを意味します。これを剥奪するには、使用後にトランザクションをコミットし、その後自分で解放するしかありません。

#相互ロック取得条件
  1. は、デッドロックが発生した場合、相互ロック取得プロセスが必要であることを意味します。つまり、ロック A を保持しているトランザクション a がロック B を取得している間に、ロック B を保持しているトランザクション b もロック A を取得しているため、最終的に相互取得が発生し、各トランザクションがブロックされます。

3. Mysql の古典的なデッドロック ケース

送金シナリオがあると仮定します。アカウント A がアカウント B に 50 元を送金すると、アカウント B もアカウント A に 30 元を送金します。このプロセスでデッドロックが発生しますか?

#3.1 テーブル作成文

CREATE TABLE `account` (
  `id` int(11) NOT NULL COMMENT '主键',
  `user_id` varchar(56) NOT NULL COMMENT '用户id',
  `balance` float(10,2) DEFAULT NULL COMMENT '余额',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='账户余额表';
#3.2 関連データの初期化

INSERT INTO `test`.`account` (`id`, `user_id`, `balance`) VALUES (1, 'A', 80.00);
INSERT INTO `test`.`account` (`id`, `user_id`, `balance`) VALUES (2, 'B', 60.00);

3.3 通常の転送プロセス

デッドロックの問題について説明する前に、見てみましょう。まずは通常の転送プロセスを参照してください。 通常の状況では、ユーザー A がユーザー B に 50 元を送金します。これは 1 回の取引で完了します。最初にユーザー A の残高とユーザー B の残高を取得する必要があります。これら 2 つのデータは後で変更する必要があるため、書き込みロックが必要です (UPDATE の場合) 他のトランザクションの変更によって変更が失われてデータがダーティになるのを防ぐためにロックします。 関連する SQL は次のとおりです:

トランザクションを開始する前に、mysql の自動送信をオフにする必要があります

set autocommit=0;
# 查看事务自动提交状态状态

show VARIABLES like 'autocommit' ;![ここに画像を挿入 説明](https://img-blog.csdnimg.cn/a486a4ed5c9d4240bd115ac7b3ce5a39.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_

#Q1NETiBA6ZqQIOmjjg==, size_20,color_FFFFFF,t_70,g_se,x_16)

<pre class="brush:php;toolbar:false"># 转账sql START TRANSACTION; # 获取A 的余额并存入A_balance变量:80 SELECT user_id,@A_balance:=balance from account where user_id = 'A' for UPDATE; # 获取B 的余额并存入B_balance变量:60 SELECT user_id,@B_balance:=balance from account where user_id = 'B' for UPDATE; # 修改A 的余额 UPDATE account set balance = @A_balance - 50 where user_id = 'A'; # 修改B 的余额 UPDATE account set balance = @B_balance + 50 where user_id = 'B'; COMMIT;</pre>実行後の結果:

データ更新は正常です

#3.4 デッドロック転送プロセスデッドロックとは何ですか? MySQL デッドロックについての理解を話しましょう

初期化された残高は次のとおりです:

このシナリオが高い同時実行下で存在すると仮定すると、ユーザー A がユーザー B に 50 元を送金すると、ユーザー B もユーザー A に 30 元を送金します。

Java プログラム操作のプロセスとタイムラインは次のとおりです: デッドロックとは何ですか? MySQL デッドロックについての理解を話しましょう

1. ユーザー A がユーザー B に 50 元を送金し、プログラムでトランザクション 1 を開く必要があります。 SQLを実行し、Aの残高を取得し、Aのデータをロックします。
# 事务1
set autocommit=0;
START TRANSACTION;
# 获取A 的余额并存入A_balance变量:80
SELECT user_id,@A_balance:=balance from account where user_id = 'A' for UPDATE;

2. ユーザー B はユーザー A に 30 元を送金します。彼はプログラムでトランザクション 2 を開いて SQL を実行し、B の残高を取得し、B のデータをロックする必要があります。

# 事务2
set autocommit=0;
START TRANSACTION;
# 获取A 的余额并存入A_balance变量:60
SELECT user_id,@A_balance:=balance from account where user_id = 'B' for UPDATE;
3. トランザクション1で残りのSQLを実行

# 获取B 的余额并存入B_balance变量:60
SELECT user_id,@B_balance:=balance from account where user_id = 'B' for UPDATE;

# 修改A 的余额
UPDATE account set balance = @A_balance - 50 where user_id = 'A';
# 修改B 的余额
UPDATE account set balance = @B_balance + 50 where user_id = 'B';
COMMIT;

トランザクション1でBデータの書き込みロックを取得した際に発生していることがわかります。タイムアウト状態が発生しました。なぜそうなるのでしょうか?主な理由は、ステップ 2 でトランザクション 2 のデータ B に対する書き込みロックを取得したため、トランザクション 2 がコミットまたはロールバックするまでは、トランザクション 1 がデータ B に対する書き込みロックを取得することはありません。

4. トランザクション 2 で残りの SQL を実行します

# 获取A 的余额并存入B_balance变量:60
SELECT user_id,@B_balance:=balance from account where user_id = 'A' for UPDATE;

# 修改B 的余额
UPDATE account set balance = @A_balance - 30 where user_id = 'B';
# 修改A 的余额
UPDATE account set balance = @B_balance + 30 where user_id = 'A';
COMMIT;

##同様にトランザクション 2 で A データの書き込みを取得します A もタイムアウトしますロック時に発生しました。データ A の書き込みロックはステップ 1 のトランザクション 1 で取得されているため、トランザクション 1 がコミットまたはロールバックするまで、トランザクション 2 はデータ A の書き込みロックを取得することはありません。

5、 为什么会出现这种情况呢?

主要是因为事务1和事务2存在相互等待获取锁的过程,导致两个事务都挂起阻塞,最终抛出获取锁超时的异常。

3.5 死锁导致的问题

众所周知,数据库的连接资源是很珍贵的,如果一个连接因为事务阻塞长时间不释放,那么后面新的请求要执行的sql也会排队等待,越积越多,最终会拖垮整个应用。一旦你的应用部署在微服务体系中而又没有做熔断处理,由于整个链路被阻断,那么就会引发雪崩效应,导致很严重的生产事故。

4、如何解决死锁问题?

要想解决死锁问题,我们可以从死锁的四个必要条件入手。
由于资源独占条件和不剥夺条件是锁本质的功能体现,无法修改,所以咱们从另外两个条件尝试去解决。

4.1 打破请求和保持条件

根据上面定义可知,出现这个情况是因为事务1和事务2同时去竞争锁A和锁B,那么我们是否可以保证锁A和锁B一次只能被一个事务竞争和持有呢?
答案是肯定可以的。下面咱们通过伪代码来看看:

/**
* 事务1入参(A, B)
* 事务2入参(B, A)
**/
public void transferAccounts(String userFrom, String userTo) {
     // 获取分布式锁
     Lock lock = Redisson.getLock();
     // 开启事务
     JDBC.excute("START TRANSACTION;");
     // 执行转账sql
     JDBC.excute("# 获取A 的余额并存入A_balance变量:80\n" +
             "SELECT user_id,@A_balance:=balance from account where user_id = '" + userFrom + "' for UPDATE;\n" +
             "# 获取B 的余额并存入B_balance变量:60\n" +
             "SELECT user_id,@B_balance:=balance from account where user_id = '" + userTo + "' for UPDATE;\n" +
             "\n" +
             "# 修改A 的余额\n" +
             "UPDATE account set balance = @A_balance - 50 where user_id = '" + userFrom + "';\n" +
             "# 修改B 的余额\n" +
             "UPDATE account set balance = @B_balance + 50 where user_id = '" + userTo + "';\n");
     // 提交事务
     JDBC.excute("COMMIT;");
     // 释放锁
     lock.unLock();
}

上面的伪代码显而易见可以解决死锁问题,因为所有的事务都是通过分布式锁来串行执行的。

那么这样就真的万事大吉了吗?

在小流量情况下看起来是没问题的,但是在高并发场景下这里将成为整个服务的性能瓶颈,因为即使你部署了再多的机器,但由于分布式锁的原因,你的业务也只能串行进行,服务性能并不因为集群部署而提高并发量,完全无法满足分布式业务下快、准、稳的要求,所以咱们不妨换种方式来看看怎么解决死锁问题。

4.2 打破相互获取锁条件(推荐)

要打破这个条件其实也很简单,那就是事务再获取锁的过程中保证顺序获取即可,也就是锁A始终在锁B之前获取。
我们来看看之前的伪代码怎么优化?

/**
* 事务1入参(A, B)
* 事务2入参(B, A)
**/
public void transferAccounts(String userFrom, String userTo) {
     // 对用户A和B进行排序,让userFrom始终为用户A,userTo始终为用户B
     if (userFrom.hashCode() > userTo.hashCode()) {
         String tmp = userFrom;
         userFrom = userTo;
         userTo = tmp;
     }
     // 开启事务
     JDBC.excute("START TRANSACTION;");
     // 执行转账sql
     JDBC.excute("# 获取A 的余额并存入A_balance变量:80\n" +
             "SELECT user_id,@A_balance:=balance from account where user_id = '" + userFrom + "' for UPDATE;\n" +
             "# 获取B 的余额并存入B_balance变量:60\n" +
             "SELECT user_id,@B_balance:=balance from account where user_id = '" + userTo + "' for UPDATE;\n" +
             "\n" +
             "# 修改A 的余额\n" +
             "UPDATE account set balance = @A_balance - 50 where user_id = '" + userFrom + "';\n" +
             "# 修改B 的余额\n" +
             "UPDATE account set balance = @B_balance + 50 where user_id = '" + userTo + "';\n");
     // 提交事务
     JDBC.excute("COMMIT;");
 }

假设事务1的入参为(A, B),事务2入参为(B, A),由于我们对两个用户参数进行了排序,所以在事务1中需要先获取锁A在获取锁B,事务2也是一样要先获取锁A在获取锁B,两个事务都是顺序获取锁,所以也就打破了相互获取锁的条件,最终完美解决死锁问题。

5、总结

因为mysql在互联网中的大量使用,所以死锁问题还是经常会被问到,希望兄弟们能掌握这方面的知识,提高自己的竞争力。

【相关推荐:mysql视频教程

以上がデッドロックとは何ですか? MySQL デッドロックについての理解を話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。