在应用程序开发中,我们要深入探讨重要的锁定问题,并介绍在ORM中实现乐观锁的示例

引入

在应用程序开发过程中,对数据进行加锁是为了保证数据的安全性而非常重要的。

本文将深入讨论应用程序中的“锁”,介绍了锁定方法的类型以及与数据库事务的关系。

摇滚是一种音乐风格

首先,锁(Lock)是一种机制,用于排除对数据同时进行读写操作。

如果不进行锁定,对于相同的数据进行多个处理时,可能会导致结果不一致。

在计算商品分类的销售总额的过程中,如果商品分类发生变化,那么每个商品分类的销售总额将与总销售额不同。

通过使用锁,可以禁止一个处理过程在另一个处理过程完成之前进行读写操作,以保证结果的一致性。

深入探究摇滚音乐

根据系统中进行锁定的“哪个级别”,需要考虑的要点和可以锁定的数据单位会有所不同。我们将按照每个级别逐步深入。

数据库级别的锁和应用程序级别的锁

DB包括MySQL、PostgreSQL、Oracle、Microsoft SQL Server、MongoDB、Cassandra、Redis等几种类型,但几乎都具备锁定功能。

在使用数据库进行锁定时,具体的行为因数据库而异。可以选择对同时进行读写(RW)的操作进行排除锁定,或者只排除写入操作的锁定等。在关系型数据库中,还可以选择对表操作进行排除锁定,或者对记录操作进行排除锁定等。

在应用程序中,通过为多个处理过程设定共同的规则,可以实现锁定。(假设没有违反规则的处理)

我们可以通过在整个应用程序中共享的内存空间中准备一个指示锁定状态的布尔变量,并通过设定下列规则来进行锁定。

    在进行对DB的读写时,必须始终参考该变量。如果锁定状态为false,则将变量设置为true,并进行对DB的读写操作,结束后将变量恢复为false。如果锁定状态为true,则一直等待直到变量变为false。

关于交易

事务是将对数据库的一系列操作作为一个处理单元进行处理的功能。
如果处理过程中出现失败,将回滚到操作开始时的状态。
处理完成称为提交。

为了确保在并发事务执行时操作的一致性,需要使用锁。在这种情况下,决定锁定哪些操作被称为事务的隔离级别,并且每个数据库都提供了几种隔离级别。

举例来说,MySQL 的隔离级别有以下几种类型。

    • READ UNCOMMITTED

 

    • READ COMMITTED

 

    • REPEATABLE READ

 

    SERIALIZABLE

我会为每一个简单解释一下。

如果将隔离级别设置为”读取未提交”,那么在事务中就可以读取到已经被改变但是还未被提交的值。

当执行了两个事务时,如果其中一个事务进行了值的更改但被回滚,另一个事务则可能读取到已更改的值,导致结果不一致的可能性存在。

将分隔级别设置为 READ COMMITTED,将会读取已提交的值。

然而,如果在执行两个事务时,一个事务修改了值,而另一个事务引用了相同的值,那么当修改值的事务完成后,另一个事务将引用修改后的值。

因此,考虑到进行值的引用的事务一方面,从事务的中间进行相同值的调用将导致结果不同。

设置分离级别为REPEATABLE READ后,无论在事务中读取值多少次,结果都保持不变。

最后,将分离级别设置为SERIALIZABLE,事务将不会并行执行,而是按顺序进行。

提高交易系统的可靠性特性

在能够进行交易的系统中,为了提高其可靠性,可以实现以下特性。
这些特性包括:表示交易本身特性的ACID特性和表示系统自身特性的BASE特性。

当试图为系统本身赋予ACID特性时,会发生可用性和性能的折衷,因此Eric Brewer提出了以可用性和性能为重点的BASE特性。 (参考信息 http://yohei-y.blogspot.com/2009/03/cap-base.html)

ACID特性和BASE特性可以共存,因此数据库可以保持ACID特性,而系统可以遵循BASE特性的配置。

酸性特征

ACID特性是指以下四个特点。

    • 原子性(Atomicity)

 

    • 一貫性(Consistency)

 

    • 独立性(Isolation)

 

    永続性(Durability)

对于每一个,简单地解释如下。

    • 原子性とはトランザクション処理が一連の処理「全てが実行された」か、あるいは「全てが実行されなかった」(実行前の状態に戻す) ことを保証する特性のこと

 

    • 一貫性とはトランザクション処理があらかじめ決められたルールに従って整合性が保たれていることを保証する特性のこと

 

    • 独立性とはトランザクション処理の途中経過が隠蔽できる特性のこと

 

    永続性とはトランザクション処理が完了したことをユーザに通知した時点で、その操作が永続化されたことを保証する特性のこと

请查阅维基百科等详细了解。

基础特征

BASE 特性指的是以下特点。

    • Basically Available

 

    • Soft-State

 

    Eventual Consistency

如果要简单解释每个,可以如下所述。

    • Basically Available とは可用性が基本である特性のこと

ロックにより処理が待機されることがないことを指します (楽観的ロック)

Soft-State とは緩いステート管理を行う特性のこと
Eventual Consistency とは途中で整合性が保たれていなくても、結果的に整合性が保たれていればよしとする特性のこと

关于乐观的摇滚音乐

到目前为止,我们对于锁的问题进行了深入探讨,并介绍了事务及其特点,其中提到了乐观锁的存在。

因此,我想在下文中介紹樂觀鎖和悲觀鎖。

首先,我们将详细介绍数据不一致的发生状态和事务隔离级别,接着我们将介绍乐观锁和悲观锁。

数据不一致

通过锁定可以防止数据的不一致性,但不一致性可能发生的情况各不相同。

为了避免所有数据的不一致性,可用性和性能会显著下降,因此需要考虑能够容忍多少不一致性,并选择相应的事务隔离级别。

发生了数据不一致的情况

从ANSI/ISO标准SQL和论文”对ANSI SQL隔离级别的批判”来看,可以列出以下问题存在的情况。

    • ダーティライト(Dirty Write)

複数のトランザクションが同じエンティティを更新した後、あるトランザクションがロールバックした場合に戻すべき値が不明となった状態である

ロストアップデート(Lost Update)

とあるトランザクションが書き込んだ値が、他のトランザクションにより上書きされた状態である

ダーティリード(Dirty Read)

とあるトランザクションが更新した値 A’ を他のトランザクションが参照した後に、更新された値がロールバックされると、読み取った値 A’ がロールバックされずにトランザクションに利用されることになってしまった状態である

ファジーリード(Fuzzy Read) / 非再現リード / ノンリピータブルリード(Non-repeatable read) ※

とあるトランザクションが読み込んだ値 A が、他のトランザクションにより A’ に更新されてコミットされると、値 A が二度と呼び出せなくなってしまった状態である。
つまり、とあるトランザクションは何度呼び出しても値 A を読み込めることを期待しているが、他のトランザクションにより更新されることで、二度と呼び出せなくなった状態である

ファントムリード(Phantom Read) ※

とあるトランザクションがテーブルを読み込んだ後に、他のトランザクションによりエンティティが挿入された場合、再度テーブルを読み込むと挿入されたエンティティが参照できてしまう状態である

リードスキュー(Read Skew) ※

とあるトランザクション A がエンティティの値 x を読み込んだ後に、他のトランザクションにより同一エンティティの値 x, y を書き込んだ上でコミットした後、トランザクション A が値 y を読み込むことで発生する、トランザクション A が持つ x は古く y は新しいという不整合である

ライトスキュー(Write Skew) ※

とあるトランザクション A がエンティティの値 x, y を読み込んだ後に、他のトランザクションにより同一エンティティの値 x, y の読み込みと新しい y を -x 等として書き込んだ上でコミットした後、トランザクション A が x を -y(この時点でy=-xなのでx) を書き込むことで発生する不整合である

如果假设在不一致状态下有「※」标记的情况,那么问题的本质就更容易理解,因为在提交之前无法访问值。(尽管它仍然是一种无法预防的状态)

只需提供一个选项,请以中文本地方式改述以下内容:
针对除了主导救援和右侧救援以外的每种状态,我们将使用以下示例来绘制出问题发生的连续步骤。

脏光

脏读是指在多个事务更新同一实体之后,如果其中一个事务回滚,则会使得应该回滚到的值变得不明确的状态。

case_dirty_write.png

根据图示,当交易A将Michael的昵称从Mick更改为Mike时,导致交易B在回滚时无法确定是应该恢复为Mick还是恢复为Mike。

丢失更新

丢失更新是指某个事务写入的值被其他事务覆盖的状态。

case_non_locking.png

如果按照图示的方式,进行以下操作:Transaction A 将 Michael 的昵称从 Mick 更改为 Mike,然后 Transaction B 将 Michael 的昵称从 Mick 更改为 Mickey,则 Transaction A 的更新将会丢失。

请将以下内容汉语母语化:

ダーティリード。

在某个事务已经更新值A’后,其他事务在读取了该值A’之后,如果更新的值A’被回滚了,那么读取的值A’却被用于事务中的情况,这就是”脏读”的状态。

case_dirty_read.png

如果在Transaction A提交之前,Transaction B读取了将Michael的昵称从Mick更改为Mike的内容,即使Transaction A回滚,Transaction B仍会继续保持昵称为Mike。

模糊带领 / 不可再现的带领 / 不可重复的带领

模糊读取指的是当一个事务读取的值A被其他事务更新为A’并且被提交后,值A将永远无法再次被调用的状态。

为了防止脏读,有时会禁止在其他事务提交之前访问实体。

case_fazzy_read_non_repeatable_read.png

当Transaction A在处理过程中读取的实体是Michael的年龄为12岁的值,而Transaction B将其更改为13岁后提交,那么Transaction A再次读取的值将变为13岁。

幽灵引导

幻读是指在某个事务读取表后,其他事务插入实体后再次读取表时,就能够看到已插入的实体的状态。

为了防止模糊读取,在事务处理期间读取实体的值始终保持一致也会发生。

case_phantom_read.png

根据图表,Transaction B 从 Prize winners 表中读取的记录数为2,因此它试图将 David 添加到当选者中。然而,由于 Transaction A 已经将 Charlie 添加为当选者并提交了,Transaction B 现在将会读取到 Charlie 被添加的新表。

事务隔离级别

我已经介绍了发生数据不一致情况的案例,但是在极端情况下,可以通过将事务串行执行而不是并行执行来防止所有的情况。

然而,为了提高效率,下面展示的事务隔离级别是由ANSI / ISO标准SQL定义的(正如上一篇文章中介绍的那样,这些隔离级别已经在MySQL等许多DBMS中实现)。

    • READ UNCOMMITTED

コミットされていない値も読み取ることが出来るよう分離する

READ COMMITTED

読み取った値は必ずコミットされた値となるよう分離する

REPEATABLE READ

とあるトランザクション処理の間、同じエンティティであればいつ読み取っても同じ値となるよう分離する

SERIALIZABLE

複数のトランザクション処理結果が、シリアルに実行された場合と同じ結果になるよう分離する

整理各个分离层级上的数据不一致性发生可能性可得以下结果。

分離レベルDirty WriteDirty ReadNon-Repeatable ReadPhantom ReadREAD UNCOMMITTED〇×××READ COMMITTED〇〇××REPEATABLE READ〇〇〇×SERIALIZABLE〇〇〇〇

<凡例>×…出现,〇…未出现

(Note: Please note that Chinese translation may vary depending on the context and specific terminology used.)

在论文《对ANSI SQL隔离级别的批评》中,重新定义和扩展了上述的ANSI/ISO SQL标准中的隔离级别,并对其进行了定义。(参考:对ANSI SQL隔离级别的批评解释公开用)

在此定义中,明确规定了 ANSI/ISO 标准 SQL 的事务隔离级别,并添加了无法通过这些隔离级别防止的数据不一致状态以及整理了每个隔离级别下可能发生的情况。

新增加的事务隔离级别有两种,快照隔离可以保持与串行化相似的隔离级别,并能够并行执行。用户可以在InterBase、Firebird、Oracle、PostgreSQL、SQL Anywhere、MongoDB、Microsoft SQL Server(2005年及以后版本)中实现快照隔离。然而,根据不同的数据库管理系统,快照隔离在Oracle中被称为可串行化等,隔离级别的名称可能有所不同。※仅供参考

    • Cursor Stability

SQL カーソルにおけるロック動作を踏まえた拡張により、READ COMMITTED では解決できない問題を防げるよう分離する

Snapshot Isolation

とある時点において取得したスナップショットに対してトランザクション操作を行うことで、ファントムリードを防げるよう分離する

不考虑分离级别的重新定义,总结事务的分离级别和数据不一致的产生与否如下。

分離レベルDirty WriteDirty ReadLost UpdateNon-Repeatable ReadPhantom ReadRead SkewWrite SkewREAD UNCOMMITTED〇××××××READ COMMITTED〇〇×××××Cursor Stability〇〇△△××△REPEATABLE READ〇〇〇〇×〇〇Snapshot Isolation〇〇〇〇△〇×SERIALIZABLE〇〇〇〇〇〇〇

<说明>×…发生,〇…不发生,△…部分发生

选择分离级别

介绍了事务隔离级别的种类以及随之而来的可防止的数据一致性问题,但结论是在任何目的和环境下都不存在一种最优的隔离级别,选择将涉及可用性和性能的权衡。

为了参考,我将介绍一些数据库管理系统中的默认隔离级别。

DBMS名デフォルトの分離レベル参考情報MySQL 8.0(5.6も同じ)
(InnoDB)REPEATABLE READ8.0, 5.6PostgreSQL 10(9.6も同じ)READ COMMITTED9.6, 10Oracle Database 18c(12cも同じ)READ COMMITTED18c, 12cMicrosoft SQL Server 2017(2016も同じ)READ COMMITTED2017, 2016MongoDB 4.0READ UNCOMMITTED4.0
※v4.0からマルチドキュメントのトランザクションが対応された

摇滚音乐的有效期限和乐观的方法论

在除了事务隔离级别之外的角度看,由于锁的有效期限的不同,也会产生不同的数据不一致性防止可能性和性能差异。

通过在事务开始时进行持续的锁定,可以增加防止不一致性的可能性,但这将增加等待时间,直到锁定被释放。

通过在读取和更新操作之间仅锁定一次,可以减少等待时间,但同时也增加了数据不一致的可能性。

以下是介绍根据同时发生的更新频率来区分的可能导致不一致的方法差异。

悲观锁

简而言之

悲観的锁是一种假设更新会经常同时进行的锁定方式,它在读取和更新处理开始时排除其他处理。

在应用程序级别执行悲观锁是可能的,但通常是在数据库级别执行。

这是适用于主要进行写入操作的用途的锁定方式。

缺点 (quē

悲观锁被视为主要进行读操作,并与无状态通信如HTTP等不兼容(可能导致长时间等待事务解锁和需要显式释放的情况发生)。

乐观锁

简而言之

乐观锁是一种用于禁止对记录进行写入的锁定方式。

这是一种基于乐观的想法,认为更新将很少同时进行的锁定方式。虽然被称为“锁定”,但实际上不对数据进行锁定,只进行竞争验证。

这是悲观派摇滚的对立方式。

锁的机制

乐观的锁在应用层面通过 ActiveRecord 或 GORM 等 O/R 映射器实现。

实施锁定的单位通常是实体单位(在关系型数据库中是表的一条记录单位)。(我认为单位取决于O/R映射器的实现,但由于同步更新的频率本来就很低的假设,所以在列单位上设置的优点很少。)

锁的工作原理非常简单。

    1. 在更新实体之前,读取每个实体设置的版本号。在实体的更新处理完成后,验证版本号是否与读取时相同。

 

    1. 【版本号未改变的情况】

 

    1. 判断没有冲突,递增版本号并进行实体的更新处理。

 

    1. 【版本号已改变的情况】

 

    判断发生了冲突,使事务处理失败。

在此,我們在上述說明中提到的「版本(顯示在列中)」這一點並沒有事先的定義,但是為了使用樂觀鎖定,我們需要在表格的記錄中準備一個顯示版本的列。

这一栏是在创建数据库表/迁移时所必需的,具体方法将取决于使用的 O/R 映射器。例如,在 Ruby on Rails 的 ActiveRecord 中,可以通过创建一个迁移文件,在模型中添加 lock_version 列,并执行迁移操作来实现。

展示了一个关于锁定操作序列的场景。

以下是未使用锁定时和乐观锁定使用时的顺序图。

当没有使用锁时

case_non_locking.png

进入丢失更新状态

乐观的使用摇滚音乐的时候

case_using_optimistic_lock.png

检测到更新处理发生了冲突,导致事务B失败。

当使用乐观锁时,如图所示,如果更新操作发生竞争,将会检测到并导致事务B失败,触发乐观锁异常(OLE)的发生。

通过比较更新处理的开始时刻和提交时刻的版本,乐观锁不对实体进行锁定,并验证更新处理是否发生了竞争。

可以避免的数据不一致

在乐观的锁机制下,可以防止脏读的发生,但是无法防止模糊读和幻读的发生。

优点

与悲观锁相比,使用乐观锁可以避免发生锁等待,并且事务完成所需的时间较短。
此外,不需要进行基于锁的阻塞和解锁(两阶段锁定)操作。

因此,需要快速响应,并且与无状态的HTTP相融合。

缺点

有一个潜在的缺点是由于没有等待锁发生,可能会增加不一致的风险。

然而,由于假定更新同时进行的频率较低,因此可以忽略这个缺点。

此外,乐观锁不是在数据库级别提供的功能,因此应用程序需要准备版本字段,并实现通过版本比较进行验证、版本写入以及乐观锁异常处理等。

然而,正如之前所述,在使用 O/R 映射器的情况下,由于提供了乐观锁定功能,因此可以忽略这个缺点。

快乐的音乐利用案例(框架/O/R映射器)

引进

迄今为止,我们已详细介绍了数据不一致的情况以及事务隔离级别,并随后介绍了乐观锁和悲观锁。

下面将使用框架和O/R映射器的具体示例介绍如何使用乐观锁。

乐观的摇滚实现案例

对于Ruby on Rails的情况

在Ruby on Rails的情况下,我们使用ActiveRecord作为对象关系映射器(O/R Mapper)。
(ActiveRecord是对象关系映射器实现模式的名称,并且使用相同的名称。)

请按以下迁移文件的方式,在模型中创建一个名为lock_version的整数类型列。

# マイグレーションファイル
$ cat db/migrate/20180906171026_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|

      t.string :name, default: ''
      t.timestamps
      t.integer :lock_version
    end
  end
end

然后,ActiveRecord 在使用 update 方法更新数据时会递增列的 lock_version,以实现乐观锁。

如果更新发生冲突,将会引发 ActiveRecord::StaleObjectError。

# update実行時のSQLで例外発生動作
## [スレッド1] ユーザ(user1) を作成
[1] pry(main)> user1 = User.create!(name: 'test')
   (0.0ms)  SAVEPOINT active_record_1
  SQL (0.6ms)  INSERT INTO "users" ("name", "created_at", "updated_at", "lock_version") VALUES (?, ?, ?, ?)  [["name", "test"], ["created_at", "2018-09-06 17:23:40.408881"], ["updated_at", "2018-09-06 17:23:40.408881"], ["lock_version", 0]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User:0x00007fb2786676f0
 id: 1,
 name: "test",
 created_at: Thu, 06 Sep 2018 17:23:40 UTC +00:00,
 updated_at: Thu, 06 Sep 2018 17:23:40 UTC +00:00,
 lock_version: 0>

## [スレッド2] ユーザ(user1) を参照
[2] pry(main)> user2 = User.find(1)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User:0x00005605bcfecaf0
 id: 1,
 name: "test",
 created_at: Thu, 06 Sep 2018 17:23:40 UTC +00:00,
 updated_at: Thu, 06 Sep 2018 17:23:40 UTC +00:00,
 lock_version: 0>

## [スレッド1] ユーザ(user1) の name を test2 に更新
[3] pry(main)> user1.update(name: 'test2')
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  UPDATE "users" SET "name" = ?, "updated_at" = ?, "lock_version" = ? WHERE "users"."id" = ? AND "users"."lock_version" = ?  [["name", "test2"], ["updated_at", "2018-09-06 17:24:07.820275"], ["lock_version", 1], ["id", 1], ["lock_version", 0]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

## [スレッド2] ユーザ(user1) の namae を test3 に更新(★例外発生)
[4] pry(main)> user2.update(name: 'test3')
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  UPDATE "users" SET "name" = ?, "updated_at" = ?, "lock_version" = ? WHERE "users"."id" = ? AND "users"."lock_version" = ?  [["name", "test3"], ["updated_at", "2018-09-06 17:24:15.752651"], ["lock_version", 1], ["id", 1], ["lock_version", 0]]
   (0.1ms)  ROLLBACK TO SAVEPOINT active_record_1
ActiveRecord::StaleObjectError: Attempted to update a stale object: User.
from /usr/local/src/rails-sample/vendor/bundle/ruby/2.5.0/gems/activerecord-5.1.6/lib/active_record/locking/optimistic.rb:95:in `_update_row'

关于Grails的情况

在Grails中,我们使用GORM作为对象关系映射工具(O/R Mapper)。
GORM在访问SQL数据库时会使用Hibernate。

默认情况下,在域类中添加了version属性,并将其用作乐观锁的版本。

# Userドメイン
package grails.sample.app

class User {

  String name

  static constraints = {
  }
}

不需要在域类中明确指定version字段,它将默认添加。(如果不想使用乐观锁,需要在映射中指定version false)

// Userドメインの name を test2 に変更
def user = User.get(1)
println 'name: ' + user.name + ', version: ' + user.version
user.name = 'test2'
user.save(flush: true)
println 'name: ' + user.name + ', version: ' + user.version

// 実行結果
name: test, version: 40
name: test2, version: 41

当有例外发生时,将会抛出 org.springframework.dao.OptimisticLockingFailureException 错误。

在使用SQLAlchemy的情况下

SQLAlchemy是一款用于Python的SQL工具包,其中包括对象关系映射器。

可以使用任意字段(如 BigInteger 类型)在模型中保存数值,就像下面的文件一样,并在 __mapper_args__ 的定义内设置 version_id_col。

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String

class User(Base):
  __tablename__ = 'users'

  id = Column('id', Integer, primary_key = True)
  name = Column('name', String(200))
  version_id = Column(BigInteger, nullable=False)

  __mapper_args__ = {
    'version_id_col': version_id
  }

然后,SQLAlchemy会在会话提交并更新数据时递增列版本号(version_id),从而实现乐观锁。

如果存在更新冲突的情况,会触发 sqlalchemy.orm.exc.StaleDataError。

# スレッド1
## モデルファイルと DB Connector の読み込み
>>> from setting import session
>>> from user import *

## User の name を test として作成
>>> user = User()
>>> user.name = 'test'
>>> session.add(user)
>>> session.commit()

## User の name を test6 へ変更
>>> user.name = 'test6'
>>> session.commit()
2018-09-11 03:34:45,280 INFO sqlalchemy.engine.base.Engine UPDATE users SET name=%s, version_id=%s WHERE users.id = %s AND users.version_id = %s
2018-09-11 03:34:45,280 INFO sqlalchemy.engine.base.Engine ('test6', 4, 1, 3)
2018-09-11 03:34:45,281 INFO sqlalchemy.engine.base.Engine COMMIT
>>> 
# スレッド2
## モデルファイルと DB Connector の読み込み
>>> from setting import session
>>> from user import *

## User を取得
>>> user = session.query(User).filter(User.id==1).first()
2018-09-11 03:33:34,195 INFO sqlalchemy.engine.base.Engine SHOW VARIABLES LIKE 'sql_mode'
2018-09-11 03:33:34,196 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 03:33:34,198 INFO sqlalchemy.engine.base.Engine SELECT DATABASE()
2018-09-11 03:33:34,198 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 03:33:34,198 INFO sqlalchemy.engine.base.Engine show collation where `Charset` = 'utf8mb4' and `Collation` = 'utf8mb4_bin'
2018-09-11 03:33:34,198 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 03:33:34,200 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS CHAR(60)) AS anon_1
2018-09-11 03:33:34,200 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 03:33:34,201 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS CHAR(60)) AS anon_1
2018-09-11 03:33:34,201 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 03:33:34,202 INFO sqlalchemy.engine.base.Engine SELECT CAST('test collated returns' AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_bin AS anon_1
2018-09-11 03:33:34,202 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 03:33:34,202 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2018-09-11 03:33:34,203 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.version_id AS users_version_id 
FROM users 
WHERE users.id = %s 
 LIMIT %s
2018-09-11 03:33:34,203 INFO sqlalchemy.engine.base.Engine (1, 1)

## User の name を test5 に変更する(★例外発生)
>>> user.name = 'test5'
>>> session.add(user)
>>> session.commit()
2018-09-11 03:35:00,504 INFO sqlalchemy.engine.base.Engine UPDATE users SET name=%s, version_id=%s WHERE users.id = %s AND users.version_id = %s
2018-09-11 03:35:00,504 INFO sqlalchemy.engine.base.Engine ('test5', 4, 1, 3)
2018-09-11 03:35:00,504 INFO sqlalchemy.engine.base.Engine ROLLBACK
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/session.py", line 943, in commit
    self.transaction.commit()
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/session.py", line 467, in commit
    self._prepare_impl()
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/session.py", line 447, in _prepare_impl
    self.session.flush()
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/session.py", line 2254, in flush
    self._flush(objects)
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/session.py", line 2380, in _flush
    transaction.rollback(_capture_exception=True)
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/util/langhelpers.py", line 66, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/util/compat.py", line 249, in reraise
    raise value
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/session.py", line 2344, in _flush
    flush_context.execute()
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/unitofwork.py", line 391, in execute
    rec.execute(self)
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/unitofwork.py", line 556, in execute
    uow
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/persistence.py", line 177, in save_obj
    mapper, table, update)
  File "/PATH/lib/python3.5/site-packages/sqlalchemy/orm/persistence.py", line 791, in _emit_update_statements
    (table.description, len(records), rows))
sqlalchemy.orm.exc.StaleDataError: UPDATE statement on table 'users' expected to update 1 row(s); 0 were matched.

在O/R映射器中实现乐观锁的存在与否。

到目前为止,我们已经看到了使用几个 O/R 映射器来实现乐观锁的具体示例。

以下是包括其他O/R映射器在内的乐观锁实现的总结。

ORM名楽観的ロックの実装有無発生する例外参考情報GORM〇org.springframework.dao.OptimisticLockingFailureExceptionGORM for Hibernate > Pessimistic and Optimistic LockingActiveRecord〇ActiveRecord::StaleObjectErrorRuby on Rails API > ActiveRecord::Locking::OptimisticSQLAlchemy〇sqlalchemy.orm.exc.StaleDataErrorSQLAlchemy > Class Mapping APIHibernate〇org.hibernate.StaleObjectStateExceptionHibernate/楽観的ロックを実装するJPA〇javax.persistence.OptimisticLockExceptionJava7 API – Class OptimisticLockExceptionEclipse Link(Toplink Essentials)〇javax.persistence.OptimisticLockException※Eclipse Link は JPA の実装である。stack overflow – what-is-the-difference-between-toplink-essentials-eclipselinkMyBATIS(旧iBATIS)×-MyBatisで行ロック(悲観ロック)を行うには?Django の ORM×-
Sequelize〇OptimisticLockErrorSequelize > Model definition > Optimistic Locking

关于NoSQL数据库中乐观锁的实现

在NoSQL中,没有事务处理的情况下,经常使用乐观锁。
(CAS(Check and Set)是Memcached中指代乐观锁的术语)
※参考:Slide Share – db-tech-showcase-mongodbP.44-50 附近

在对象关系映射(O/R Mapper)中是否实现了乐观锁机制。

ORM名楽観的ロックの実装有無発生する例外参考情報Mongoose〇VersionErrormongoose > versionKey

总结

在本文中,我们首先深入探讨了音乐的主题,并介绍了交易和其特点。

接下来介绍了存在大量数据不一致情况的状态,并需要在可用性和效率之间权衡选择事务的隔离级别。

此外,我介绍了一种基于乐观主义思维的乐观锁方法,并指出它在需要快速读取处理的无状态处理HTTP中非常有效。

最后,我们举了几个具体的例子来介绍如何利用乐观锁在OR Mapper中实现,并总结了在OR Mapper中是否实现了乐观锁。

希望这篇文章能对大家理解和复习软件中重要的锁有所帮助。

广告
将在 10 秒后关闭
bannerAds