![](/img/trans.png)
[英]Concurrent updates in Spring Service with isolation=REPEATABLE_READ
[英]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_READ
和SERLIAZABLE
都对所有要读取的行/表设置了读锁,对所有正在修改的行设置了写锁。
如果这种理解是正确的,那么在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.