繁体   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