数据库初学者
介绍
数据库是高度并发的系统 。 总是有更新冲突的机会例如当两个并发的事务尝试更新同一条记录时。 如果在任何时候只有一个数据库事务那么所有操作将顺序执行。 当多个事务试图更新相同的数据库行时我们将面临挑战因为我们仍然必须确保一致的数据状态转换。
SQL标准定义了三个一致性异常现象
- 脏读 通过提交读可重复读和可序列化隔离级别防止
- 不可重复读取 由可重复读取和可序列化隔离级别阻止
- 幻像读取 可序列化隔离级别阻止
鲜为人知的现象是丢失的更新异常这就是我们将在本文中讨论的内容。
隔离度
大多数数据库系统将“已提交读”作为默认隔离级别MySQL使用“可重复读”代替。 选择隔离级别是为了找到满足我们当前应用程序需求的一致性和可伸缩性之间的平衡。
以下所有示例都将在PostgreSQL 9.3上运行。 其他数据库系统可能会根据其特定的ACID实现而有所不同。
PostgreSQL同时使用锁和MVCC多版本并发控制 。 在MVCC中读取和写入锁定没有冲突因此读取不会阻止写入写入也不会阻止读取。
由于大多数应用程序都使用默认的隔离级别因此了解“读取已提交”特征非常重要
- 查询仅查看查询开始之前已提交的数据以及当前事务未提交的更改
- 查询执行期间提交的并发更改对当前查询不可见
- UPDATE / DELETE语句使用锁来防止并发修改
如果两个事务尝试更新同一行则第二个事务必须等待第一个事务提交或回滚并且如果第一个事务已提交则必须重新评估第二个事务DML WHERE子句以查看是否匹配仍然相关。
在此示例中Bob的UPDATE必须等待Alice的事务结束提交/回滚才能继续进行。
与其他更严格的隔离级别相比“已提交读”容纳更多的并发事务但是较少的锁定导致丢失更新的机会更大。
更新丢失
如果两个事务正在更新同一行的不同列则没有冲突。 第二次更新将阻塞直到提交第一个事务并且最终结果将反映这两个更新更改。
如果两个事务要更改相同的列则第二个事务将覆盖第一个事务因此失去了第一个事务的更新。
因此当用户覆盖当前数据库状态而没有意识到在数据加载到更新之间其他人对其进行了更改时更新将丢失。
在此示例中Bob不知道Alice刚刚将数量从7更改为6因此她的UPDATE被Bob的更改覆盖。
典型的查找-修改-刷新ORM策略
Hibernate像任何其他ORM工具一样自动将实体状态转换转换为SQL查询 。 您首先加载一个实体对其进行更改然后让Hibernate刷新机制将所有更改与数据库同步。
public Product incrementLikes(Long id) {Product product entityManager.find(Product.class, id);product.incrementLikes(); return product;}public Product setProductQuantity(Long id, Long quantity) {Product product entityManager.find(Product.class, id);product.setQuantity(quantity);return product;}
正如我已经指出的那样所有UPDATE语句都获得写锁即使是在“读提交”隔离中也是如此。 持久性上下文后写策略旨在减少锁定保持时间间隔但是读操作和写操作之间的时间间隔越长进入丢失更新情况的机会就越大。
Hibernate在UPDATE语句中包括所有行列。 可以更改此策略以仅包含脏属性通过DynamicUpdate批注但是参考文档警告我们其有效性
尽管这些设置在某些情况下可以提高性能但实际上在其他情况下却可以降低性能。
因此让我们看看Alice和Bob如何使用ORM框架同时更新同一产品
爱丽丝 鲍勃 store 开始 store 选择*来自产品 WHERE ID 1; ID | 赞| 数量 --- ——- ------ 1 | 5 | 7 1行 store 开始 store SELECT * FROM PRODUCT WHERE ID 1; ID | 赞| 数量 --- ——- ------ 1 | 5 | 7 1行 store UPDATE PRODUCT SETLIKESQUANTITY67 WHERE ID 1; store UPDATE PRODUCT SETLIKESQUANTITY510 WHERE ID 1; store COMMIT; store SELECT * FROM PRODUCT WHERE ID 1; ID | 赞| 数量 --- ——- ------ 1 | 6 | 7 1行 store COMMIT; store SELECT * FROM PRODUCT WHERE ID 1; ID | 赞| 数量 --- ——- ------ 1 | 5 | 10 1行 store SELECT * FROM PRODUCT WHERE ID 1;ID | 赞| 数量 --- ——- ------ 1 | 5 | 10 1行
再一次在鲍勃不知道他重写了她的更改的情况下爱丽丝的更新丢失了。 我们应该始终防止数据完整性异常所以让我们看看如何克服这种现象。
可重复读
使用重复读取以及提供更严格隔离级别的可序列化可以防止并发数据库事务中的更新丢失。
爱丽丝 鲍勃 store 开始 store SET事务隔离级别可重复读取 store SELECT * FROM PRODUCT WHERE ID 1; ID | 赞| 数量 --- ——- ------ 1 | 5 | 7 1行 store 开始 store SET事务隔离级别可重复读取 store SELECT * FROM PRODUCT WHERE ID 1; ID | 赞| 数量 --- ——- ------ 1 | 5 | 7 1行 store UPDATE PRODUCT SETLIKESQUANTITY67WHERE ID 1; store UPDATE PRODUCT SETLIKESQUANTITY510WHERE ID 1; store COMMIT; store SELECT * FROM PRODUCT WHERE ID 1; ID | 赞| 数量 --- ——- ------ 1 | 6 | 7 1行 错误由于并发更新无法序列化访问 store SELECT * FROM PRODUCT WHERE ID 1; 错误当前事务中止命令被忽略直到事务块结束 1行这次Bob无法覆盖Alice的更改并且他的交易被中止。 在“可重复读取”中查询将看到当前事务开始时的数据快照。 其他并发事务提交的更改对当前事务不可见。
如果两个事务试图修改同一记录则第二个事务将等待第一个事务提交或回滚。 如果第一个事务提交则第二个事务必须中止以防止丢失更新。
选择更新
另一种解决方案是将FOR UPDATE与默认的Read Committed隔离级别一起使用。 该锁定子句获得与UPDATE和DELETE语句相同的写锁定。
爱丽丝 鲍勃 store 开始 store SELECT * FROM PRODUCT WHERE ID 1 FOR UPDATE; ID | 赞| 数量 --- ——- ------ 1 | 5 | 7 1行 store 开始 store SELECT * FROM PRODUCT WHERE ID 1 FOR UPDATE; store UPDATE PRODUCT SETLIKESQUANTITY67WHERE ID 1; store COMMIT; store SELECT * FROM PRODUCT WHERE ID 1; ID | 赞| 数量 --- ——- ------ 1 | 6 | 7 1行 id | 喜欢| 数量 --- ——- ------ 1 | 6 | 7 1行store UPDATE PRODUCT SETLIKESQUANTITY610WHERE ID 1; 更新1 store COMMIT; 承诺 store SELECT * FROM PRODUCT WHERE ID 1; id | 喜欢| 数量 --- ——- ------ 1 | 6 | 10 1列Bob无法继续执行SELECT语句因为Alice已经获得了同一行的写锁。 鲍勃将不得不等待爱丽丝结束交易并且当鲍勃的SELECT解除阻止时他将自动看到她的更改因此爱丽丝的UPDATE不会丢失。
两个事务都应使用FOR UPDATE锁定。 如果第一个事务没有获得写锁丢失的更新仍然可能发生。
爱丽丝 鲍勃 store 开始 store SELECT * FROM PRODUCT WHERE ID 1; id | 喜欢| 数量 --- ——- ------ 1 | 5 | 7 1列 store 开始 store 选择*从产品ID 1进行更新 id | 喜欢| 数量 --- ——- ------ 1 | 5 | 7 1列 store UPDATE PRODUCT SETLIKESQUANTITY67WHERE ID 1; store UPDATE PRODUCT SETLIKESQUANTITY610WHERE ID 1; store SELECT * FROM PRODUCT WHERE ID 1; id | 喜欢| 数量 --- ——- ------ 1 | 6 | 10 1列 store COMMIT; store SELECT * FROM PRODUCT WHERE ID 1;id | 喜欢| 数量 --- ——- ------ 1 | 6 | 7 1列
store COMMIT;
store SELECT * FROM PRODUCT WHERE ID 1;id | 喜欢| 数量 --- ——- ------ 1 | 6 | 7 1列
爱丽丝的UPDATE被阻止直到鲍勃在当前事务结束时释放写锁为止。 但是Alice的持久性上下文使用的是陈旧的实体快照因此她覆盖了Bob的更改从而导致另一种丢失更新的情况。
乐观锁
我最喜欢的方法是用乐观锁定机制代替悲观锁定。 与MVCC一样乐观锁定定义了版本控制并发控制模型该模型可以在不获取其他数据库写锁定的情况下工作。
产品表还将包括一个版本列该列可防止旧数据快照覆盖最新数据。
爱丽丝 鲍勃 store 开始 开始 store SELECT * FROM PRODUCT WHERE ID 1; id | 喜欢| 数量 版 - ——- -——- - 1 | 5 | 7 | 2 1列 store 开始 开始 store SELECT * FROM PRODUCT WHERE ID 1; id | 喜欢| 数量 版 - ——- -——- - 1 | 5 | 7 | 2 1列 store UPDATE PRODUCT SETLIKESQUANTITYVERSION673WHEREIDVERSION12; 更新1 store UPDATE PRODUCT SETLIKESQUANTITYVERSION5103WHEREIDVERSION12; store COMMIT; store SELECT * FROM PRODUCT WHERE ID 1; id | 喜欢| 数量 版 - ——- -——- - 1 | 6 | 7 | 3 1列 更新0 store COMMIT; store SELECT * FROM PRODUCT WHERE ID 1; id | 喜欢| 数量 版 - ——- -——- - 1 | 6 | 7 | 3 1列每个UPDATE都会将加载时版本放入WHERE子句中假设自从数据库检索到该行以来没有人更改过该行。 如果其他某个事务设法提交较新的实体版本则UPDATE WHERE子句将不再与任何行匹配因此可以防止丢失更新。
Hibernate使用PreparedStatementexecuteUpdate结果检查更新的行数。 如果没有匹配的行则抛出StaleObjectStateException 使用Hibernate API时或OptimisticLockException 使用JPA时。
与“可重复读取”一样就原子性保证而言当前事务和持久性上下文将中止。
结论
除非您计划防止此类情况否则可能会丢失更新。 除了乐观锁定之外当SELECT和UPDATE语句都在同一物理事务中执行时所有悲观锁定方法仅在同一数据库事务的范围内有效。
在我的下一篇文章中我将解释为什么在使用应用程序级事务时乐观锁定是唯一可行的解决方案就像大多数Web应用程序一样。
翻译自: https://www.javacodegeeks.com/2014/09/a-beginners-guide-to-database-locking-and-the-lost-update-phenomena.html
数据库初学者