簡體   English   中英

為什么Springboot Transaction中隔離級別REPEATABLE_READ和SERIALIZABLE允許並發讀而不是順序讀?

[英]Why do isolation levels REPEATABLE_READ and SERIALIZABLE in Springboot Transaction allow concurrent read instead of sequential read?

我試圖了解事務隔離級別通常是如何工作的。 因此,我創建了一個簡單的 Springboot 應用程序,其中 MySQL 作為數據庫,每次調用 API 時,數據庫中特定行的值基本上都會增加 1。

我有以下TestEntity class

@Entity(name = "test")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TestEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(unique = true)
    private String ipAddress;

    private Long value;
}

我有以下 API 允許與數據庫通信

@GetMapping("db")
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void testDb(final HttpServletRequest request) {
  final String ipAddress = request.getHeader("X-Real-IP");
  log.debug("Received IP: {}", ipAddress);
  final var testEntityOptional = testRepository.findByIpAddress(ipAddress);
  testEntityOptional.ifPresent((testEntity -> {
    log.debug("Retrieved Test Entity: {}", testEntity);
    testEntity.setValue(testEntity.getValue() + 1);
  }));
}

您會注意到我已將隔離級別設置為REPEATABLE_READ

數據庫的初始 state:

id|ip_address|value|
--+----------+-----+
30|1.0.0.2   |    1|

當我使用 Apache JMeter 通過 5 個並發請求命中此 API 時,我在控制台中得到以下 output:

2021-09-25 14:11:48.021 DEBUG 25981 --- [nio-8001-exec-3] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:11:48.021 DEBUG 25981 --- [nio-8001-exec-4] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:11:48.021 DEBUG 25981 --- [nio-8001-exec-5] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:11:48.021 DEBUG 25981 --- [nio-8001-exec-2] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:11:48.021 DEBUG 25981 --- [nio-8001-exec-1] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:11:48.024 DEBUG 25981 --- [nio-8001-exec-4] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=1)
2021-09-25 14:11:48.024 DEBUG 25981 --- [nio-8001-exec-3] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=1)
2021-09-25 14:11:48.024 DEBUG 25981 --- [nio-8001-exec-5] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=1)
2021-09-25 14:11:48.024 DEBUG 25981 --- [nio-8001-exec-1] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=1)
2021-09-25 14:11:48.024 DEBUG 25981 --- [nio-8001-exec-2] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=1)

下面是請求后的DB state:

id|ip_address|value|
--+----------+-----+
30|1.0.0.2   |    2|

您可以看到值更新為 2 而不是 6 (5+1)

當我以隔離級別SERIALIZABLE運行它時,我得到以下 output

2021-09-25 14:17:12.227 DEBUG 25981 --- [nio-8001-exec-5] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:17:12.227 DEBUG 25981 --- [nio-8001-exec-9] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:17:12.227 DEBUG 25981 --- [nio-8001-exec-1] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:17:12.227 DEBUG 25981 --- [io-8001-exec-10] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:17:12.228 DEBUG 25981 --- [nio-8001-exec-5] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=1)
2021-09-25 14:17:12.228 DEBUG 25981 --- [io-8001-exec-10] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=1)
2021-09-25 14:17:12.228 DEBUG 25981 --- [nio-8001-exec-1] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=1)
2021-09-25 14:17:12.228 DEBUG 25981 --- [nio-8001-exec-9] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=1)
2021-09-25 14:17:12.229  WARN 25981 --- [io-8001-exec-10] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2021-09-25 14:17:12.229 ERROR 25981 --- [io-8001-exec-10] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2021-09-25 14:17:12.230 ERROR 25981 --- [io-8001-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement] with root cause

com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at ........................

2021-09-25 14:17:12.227 DEBUG 25981 --- [nio-8001-exec-8] i.t.t.a.controllers.TestController       : Received IP: 1.0.0.2
2021-09-25 14:17:12.229  WARN 25981 --- [nio-8001-exec-5] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2021-09-25 14:17:12.229  WARN 25981 --- [nio-8001-exec-9] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2021-09-25 14:17:12.231 ERROR 25981 --- [nio-8001-exec-9] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2021-09-25 14:17:12.231 ERROR 25981 --- [nio-8001-exec-5] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2021-09-25 14:17:12.232 ERROR 25981 --- [nio-8001-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement] with root cause

com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at ........................

2021-09-25 14:17:12.232 ERROR 25981 --- [nio-8001-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement] with root cause

com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at ........................

2021-09-25 14:17:12.250 DEBUG 25981 --- [nio-8001-exec-8] i.t.t.a.controllers.TestController       : Retrieved Test Entity: TestEntity(id=30, ipAddress=1.0.0.2, value=2)

DB state 從

id|ip_address|value|
--+----------+-----+
30|1.0.0.2   |    1|

id|ip_address|value|
--+----------+-----+
30|1.0.0.2   |    3|

當我簽入數據庫時,我看到值為 3,這意味着 5 個線程中有兩個能夠讀取更新后的值,然后正確遞增。

如果我錯了,請糾正我,但據我所知, REPEATABLE_READSERLIAZABLE都對所有要讀取的行/表設置了鎖,對所有正在修改的行設置了寫鎖

如果這種理解是正確的,那么在REPEATABLE_READ的情況下,所有 5 個線程如何讀取相同的數據? 讀鎖不應該阻止其他 4 個線程讀取數據庫嗎? 讀取不應該以順序方式進行,只有在一個線程完成讀/寫后另一個線程才能訪問它嗎?

為什么在SERIALIZABLE的情況下,有兩個線程能夠執行預期的行為,即能夠讀取更新的值然后遞增,而其他 3 個線程不能? 在我看來,我在 REPEATABLE_READ 中期待的可序列化讀取正在這里發生,但只是部分發生

我在這里錯過了什么?

感謝您花時間閱讀

這取決於所使用的數據庫引擎。 我猜你正在使用 InnoDB,因為它是 MySQL 的默認存儲引擎。

傳統的基於鎖的數據庫,它們處理行、頁上的鎖,它們最終甚至可以升級為完全成熟的表鎖。

但是大多數現代數據庫(如 InnoDB)都使用多版本並發控制,並且讀取和寫入可以同時發生。 因此,想象一下某個表中的以下 2 個條目:

X=1
Y=1

並且有 2 個事務 A 和 B 使用 Serializable 隔離級別。 並且事務 A 已讀取 X=1 並且事務 B 進來並遞增 Y,它將鎖定行 Y 直到它提交。 當事務A要讀取Y時,它並不關心Y最新更新的版本(並且可以忽略鎖)。 它可以使用撤消/重做日志重建 Y=1 的值。

這個想法是只有作者會阻止其他作者,但他們不會阻止讀者。 Tom Kyte 寫了一些關於 Oracle 和 MVCC 的優秀書籍。

回答你的問題:

在日志中你可以看到:

2021-09-25 14:17:12.230 ERROR 25981 --- [io-8001-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement] with root cause

com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at ........................

所以我的理解是一筆交易能夠讀取某些記錄的價值。 一個不同的交易進來更新記錄。 當第一個事務要寫入時,它看到它讀取的版本比當前提交的版本舊。 因此不會獲得鎖。

所以沿着這些方向:

Transaction 1: start
Transaction 1: read record (value 1) and version 1.
Transaction 2: start
Transaction 2: read record (value 1) and version 1
Transaction 2: lock record with version 1
Transaction 2: write value=2 to record.
Transaction 2: commit: increment the record to version 2
Transaction 2: lock record with version 1  <----- boom

這些版本由 MVCC 數據庫引擎跟蹤。 信息被添加到每一行。

如果您將觸發“選擇更新”而不是常規的“選擇”,則可以避免此問題,因為 select 將導致記錄鎖定並且您不會遇到這種樂觀鎖定失敗。

PS:我已經很長時間沒有使用常規數據庫了。 所以我的知識可能有點不穩定。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM