通过使用正确的锁定策略,可以解决并发访问共享数据的挑战,确保应用程序平稳运行,并避免可能导致数据损坏或不一致结果的冲突。
在本文中,我们将探讨如何使用Kotlin、Ktor和jOOQ实现悲观锁和乐观锁,并提供实际示例,帮助您了解何时使用每种方法。
无论您是初学者还是经验丰富的开发人员,我们的目标是让您深入了解并发控制的原则,并学会如何在实践中应用它们。
让我们假设我们在MySQL数据库中有一个名为users的表,结构如下:
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
age INT NOT NULL,
PRIMARY KEY (id)
);
我们希望在更新用户年龄时实现悲观锁定,这意味着当我们从数据库中读取用户行时,我们希望锁定该行,并在完成更新后保持锁定。这确保在我们处理该行时,没有其他事务可以更新相同的行。
首先,我们需要请求jOOQ在查询users表时使用悲观锁定。
我们可以通过在SELECT查询上设置forUpdate()标志来实现:
val user = dslContext.selectFrom(USERS)
.where(USERS.ID.eq(id))
.forUpdate()
.fetchOne()
当我们执行该查询时,这将锁定具有指定ID的用户行。
接下来,我们可以更新用户的年龄并提交事务:
dslContext.update(USERS)
.set(USERS.AGE, newAge)
.where(USERS.ID.eq(id))
.execute()
transaction.commit()
请注意,我们需要在与用于读取用户行并锁定它的事务中执行更新操作。这确保在提交事务时释放锁定。您可以在下一节中看到如何实现这一点。
最后,这是一个示例的Ktor端点,演示如何使用此代码更新用户的年龄:
post("/users/{id}/age") {
val id = call.parameters["id"]?.toInt() ?: throw BadRequestException("Invalid ID")
val newAge = call.receive()
dslContext.transaction { transaction ->
val user = dslContext.selectFrom(USERS)
.where(USERS.ID.eq(id))
.forUpdate()
.fetchOne()
if (user == null) {
throw NotFoundException("User not found")
}
user.age = newAge
dslContext.update(USERS)
.set(USERS.AGE, newAge)
.where(USERS.ID.eq(id))
.execute()
transaction.commit()
}
call.respond(HttpStatusCode.OK)
}
正如您所见,我们首先使用jOOQ的forUpdate()方法读取用户行并对其进行锁定。然后我们检查用户是否存在,更新他们的年龄,并提交事务。最后,我们以HTTP 200 OK状态代码作为响应,表示操作成功。
乐观锁定是一种技术,在读取行时不对其进行锁定,而是在更新时为行添加一个版本号,并在更新时检查该版本号。如果自从读取行后版本号发生了更改,这意味着在此期间有其他人对其进行了更新,我们需要使用更新后的行重试操作。
要实现乐观锁定,我们需要在用户表中添加一个版本列:
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
age INT NOT NULL,
version INT NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
我们将使用版本列来跟踪每一行的版本。
现在,让我们更新我们的Ktor端点以使用乐观锁定。首先,我们将读取用户行并检查其版本:
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
age INT NOT NULL,
version INT NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
在这个例子中,我们使用一个while循环来重试更新,直到成功使用正确的版本号更新行。首先,我们读取用户行并获取其当前的版本号。然后,我们更新用户的年龄并增加版本号。最后,我们执行更新查询并检查更新了多少行。
如果更新成功(即更新了一行),我们将updated设置为true并退出循环。如果更新失败(即没有更新任何行,因为版本号已经发生变化),我们重复循环并重试。
请注意,在WHERE子句中使用and(USERS.VERSION.eq(oldVersion))条件,以确保只有在版本号仍然与之前读取的版本号相同的情况下才更新该行。
乐观锁和悲观锁是并发控制中使用的两种重要技术,用于确保多用户环境中的数据一致性和正确性。
悲观锁在修改记录时阻止其他用户访问该记录,而乐观锁允许多个用户并发访问和修改数据。
处理账户之间的资金转账的银行应用是一个悲观锁更好选择的典型示例。在这种情况下,当用户发起转账时,系统应确保账户中的资金可用,并且没有其他用户同时修改同一账户的余额。
在这种情况下,至关重要的是在事务进行时阻止其他用户访问该账户。应用程序可以使用悲观锁来确保在转账过程中对账户进行独占访问,防止并发更新,从而确保数据的一致性。
管理产品库存的在线购物应用是一个乐观锁更好选择的典型示例。
在这种情况下,多个用户可以同时访问同一产品页面并进行购买。当用户将产品添加到购物车并进行结算时,系统应确保产品的可用性是最新的,并且没有其他用户购买了同一产品。
无需锁定产品记录,因为系统可以在结算过程中处理冲突。应用程序可以使用乐观锁,允许对产品记录进行并发访问,并通过检查产品的可用性并相应地更新库存来解决事务中的冲突。
在设计和实现数据库系统时,了解悲观锁和乐观锁策略的优点和局限性非常重要。
虽然悲观锁是确保数据一致性的可靠方式,但它可能会导致性能和可伸缩性下降。另一方面,乐观锁提供了更好的性能和可伸缩性,但需要仔细考虑并发问题和错误处理。
选择适合的锁策略最终取决于具体的用例和数据一致性与性能之间的权衡。了解两种锁策略对于做出良好的决策和构建强大可靠的后端系统至关重要。
作者:Jakub JRZ
更多技术干货请关注公号“云原生数据库”
squids.cn,基于公有云基础资源,提供云上 RDS,云备份,云迁移,SQL 窗口门户企业功能,
帮助企业快速构建云上数据库融合生态。