悲观锁 引起死锁
有时您根本无法避免:通过SQL进行悲观锁定。 实际上,当您要在共享的全局锁上同步多个应用程序时,它是一个很棒的工具。
有人可能认为这是在滥用数据库。 如果可以解决您遇到的问题,我们认为可以使用您拥有的工具。 例如, RDBMS可能是消息队列的完美实现 。
假设您确实有悲观的锁定用例,并且您确实想选择RDBMS。 现在,如何正确处理? 因为这很容易产生死锁。 想象一下以下设置(我正在为此使用Oracle):
CREATE TABLE locks (v NUMBER(18));INSERT INTO locks
SELECT level
FROM dual
CONNECT BY level <= 10;
这将生成10条记录,我们将它们用作10个不同的行级锁。
现在,让我们从两个sqlplus客户端连接到数据库:
实例1
SQL> SELECT *2 FROM locks3 WHERE v = 14 FOR UPDATE;V
----------1
实例2
SQL> SELECT *2 FROM locks3 WHERE v = 24 FOR UPDATE;V
----------2
现在,我们从两个不同的会话中获得了两个不同的锁。
然后,让我们反过来:
实例1
SQL> SELECT *2 FROM locks3 WHERE v = 24 FOR UPDATE;
实例2
SQL> SELECT *2 FROM locks3 WHERE v = 14 FOR UPDATE;
现在这两个会话都被锁定,幸运的是,Oracle将检测到这并使其中一个会话失败:
ORA-00060: deadlock detected while waiting for resource
避免死锁
这是一个非常明确的示例,在此示例中,很容易看到它发生的原因以及潜在的避免方法。 避免死锁的一种简单方法是建立一个规则,即始终必须按升序获取所有锁。 如果您知道需要1号和2号锁,则必须按顺序购买它们。 这样,您仍然会产生锁定并因此产生争用,但是一旦负载减少,至少争用将最终(可能)得到解决。 这是一个示例,显示了当您有更多客户时会发生什么。 这次,以Java线程编写。
在示例中,我们将jOOλ用于更简单的lambda表达式(例如lambdas抛出检查的异常)。 当然,我们将大量滥用Java 8!
Class.forName("oracle.jdbc.OracleDriver");// We want a collection of 4 threads and their
// associated execution counters
List<Tuple2<Thread, AtomicLong>> list =
IntStream.range(0, 4)// Let's use jOOλ here to wrap checked exceptions// we'll map the thread index to the actual tuple.mapToObj(Unchecked.intFunction(i -> {final Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:xe", "TEST", "TEST");final AtomicLong counter = new AtomicLong();final Random rnd = new Random();return Tuple.tuple(// Each thread acquires a random number of// locks in ascending ordernew Thread(Unchecked.runnable(() -> {for (;;) {String sql =" SELECT *"+ " FROM locks"+ " WHERE v BETWEEN ? AND ?"+ " ORDER BY v"+ " FOR UPDATE";try (PreparedStatement stmt = con.prepareStatement(sql)) {stmt.setInt(1, rnd.nextInt(10));stmt.setInt(2, rnd.nextInt(10));stmt.executeUpdate();counter.incrementAndGet();con.commit();}}})),counter);})).collect(Collectors.toList());// Starting each thread
list.forEach(tuple -> tuple.v1.start());// Printing execution counts
for (;;) {list.forEach(tuple -> {System.out.print(String.format("%1s:%2$-10s",tuple.v1.getName(),tuple.v2.get()));});System.out.println();Thread.sleep(1000);
}
在程序运行时,您可以看到它逐渐地继续运行,每个线程承担与其他线程大致相同的负载:
Thread-1:0 Thread-2:0 Thread-3:0 Thread-4:0
Thread-1:941 Thread-2:966 Thread-3:978 Thread-4:979
Thread-1:2215 Thread-2:2206 Thread-3:2244 Thread-4:2253
Thread-1:3422 Thread-2:3400 Thread-3:3466 Thread-4:3418
Thread-1:4756 Thread-2:4720 Thread-3:4855 Thread-4:4847
Thread-1:6095 Thread-2:5987 Thread-3:6250 Thread-4:6173
Thread-1:7537 Thread-2:7377 Thread-3:7644 Thread-4:7503
Thread-1:9122 Thread-2:8884 Thread-3:9176 Thread-4:9155
现在,为了论证,让我们来做禁止的事情和ORDER BY DBMS_RANDOM.VALUE
String sql =" SELECT *"
+ " FROM locks"
+ " WHERE v BETWEEN ? AND ?"
+ " ORDER BY DBMS_RANDOM.VALUE"
+ " FOR UPDATE";
用不了多长时间,您的应用程序就会爆炸:
Thread-1:0 Thread-2:0 Thread-3:0 Thread-4:0
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:90
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:90
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:90
Exception in thread "Thread-3" org.jooq.lambda.UncheckedException:
java.sql.SQLException: ORA-00060: deadlock detected while waiting for resourceThread-1:72 Thread-2:79 Thread-3:79 Thread-4:93
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:93
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:93
Exception in thread "Thread-1" org.jooq.lambda.UncheckedException:
java.sql.SQLException: ORA-00060: deadlock detected while waiting for resourceThread-1:72 Thread-2:1268 Thread-3:79 Thread-4:1330
Thread-1:72 Thread-2:3332 Thread-3:79 Thread-4:3455
Thread-1:72 Thread-2:5691 Thread-3:79 Thread-4:5841
Thread-1:72 Thread-2:8663 Thread-3:79 Thread-4:8811
Thread-1:72 Thread-2:11307 Thread-3:79 Thread-4:11426
Thread-1:72 Thread-2:12231 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:12231 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:12231 Thread-3:79 Thread-4:12348
Exception in thread "Thread-4" org.jooq.lambda.UncheckedException:
java.sql.SQLException: ORA-00060: deadlock detected while waiting for resourceThread-1:72 Thread-2:13888 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:17037 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:20234 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:23495 Thread-3:79 Thread-4:12348
最后,由于死锁异常,除一个线程之外的所有线程都被杀死了(至少在我们的示例中)。
不过要当心竞争
在显示悲观锁定(或一般而言锁定)的其他负面影响方面,上述示例也令人印象深刻:争用。 在“不良示例”中继续执行的单个线程几乎与之前的四个线程一样快。 我们使用随机锁定范围的愚蠢示例导致这样一个事实,即平均而言,几乎每次获取锁定的尝试至少都会产生一些阻塞 。 您如何解决? 通过查找enq:TX –您会话中的行锁争用事件。 例如:
SELECT blocking_session, event
FROM v$session
WHERE username = 'TEST'
上面的查询返回灾难性的结果,在这里:
BLOCKING_SESSION EVENT
-------------------------------------
48 enq: TX - row lock contention
54 enq: TX - row lock contention
11 enq: TX - row lock contention
11 enq: TX - row lock contention
结论
结论只能是:谨慎使用悲观锁定,并始终期待意外发生 。 在进行悲观锁定时,死锁和大量争用都是您可能遇到的问题。 作为一般经验法则,请遵循以下规则(按顺序):
- 如果可以,请避免悲观锁定
- 如果可以,请避免在每个会话中锁定多于一行
- 如果可以,请避免以随机顺序锁定行
- 避免去上班看看发生了什么
翻译自: https://www.javacodegeeks.com/2015/04/how-to-avoid-the-dreaded-dead-lock-when-pessimistic-locking-and-some-awesome-java-8-usage.html
悲观锁 引起死锁