前置知识
数据库事务:是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。refer
事务的四大基本要素:ACID refer
划重点:
在ACID四大要素中,Isolation要素确定了其他用户和系统对事务完整性的可见性。而事务的并发所带来的问题往往与Isolation要素的执行情况有关。
事务并发可能带来的问题
- 脏读(dirty read)(w-w conflict): A事务修改数据并回滚,但在回滚之前数据已经被B事务读取
- 不可重复读(R-W-R conflict): 在一个事务过程中,前后两次读到的数据不一致(被中途被其他事务修改)
- 丢失提交(lost update problem, W-W conflict): 两个事务同时对一个数据进行读写操作(e.g. 取出数据并+1),可能导致其中一个事务的写操作未达到预期效果,像是丢失一样。
- 幻读(incorrect summary problem):A事务修改所有数据行之后,B事务插入新数据,A再查询总行数。
如何解决
方案1: 设置数据库级别
划重点:
为了解决事务并发引起的问题,SQL标准定义了4个事务隔离级别作为数据库层面的标准: refer
- Read Uncommitted
- Read Committed
- Repeatable Read
-
Serializable
事务隔离级别在各个数据库的实现方式不同,但都符合SQL的标准。
以MySql为例,默认的事务隔离级别为:Repeatable Read。
通过设置事务隔离级别,可以解决事务并发带来的问题。但在实际应用中并不一定是最好的解决办法,因为:
- 隔离级别越高,并发性能越低。
-
数据库事务隔离级别的设置是全局设置,会影响整个数据库并发性能。
方案2: 对数据(表或行)加锁
前置知识
- 乐观锁和悲观锁 refer
数据库事务隔离级别的实现本质还是锁机制,只是数据库可以根据隔离级别的设置来隐式的加解锁。
数据库层面的显式悲观锁
除了设置数据库事务隔离级别之外(隐式的加解锁),我们还可以在执行某些事务的过程中通过“显式加锁“来解决并发问题。
以MySql为例:refer
- 如果读取出来的结果集需要修改后再提交,需使用select ... for update读取结果集。(用于解决“丢失提交”的问题)
- 当有关联关系的两个实体可能同时新增时,一方因新增实体修改关联关系,需使用select ... in share mode查询另一方数据进行关联关系的更新。
以上两种加锁方式,本质上都是数据库层面的悲观锁实现。
应用层面的乐观锁
乐观锁的实现需要配合数据库的update语句来实现。在进行update这个原子操作的同时进行version或timestamp的验证。如果发现版本已更新,则回退。
Note: Mysql数据库的Update语句中嵌套子查询(查询其他表)的情况下下,不会锁子查询涉及的表。所以,在有查A表版本,更新B表这样的操作时,只能通过select ..for update这样的悲观锁将A表的数据进行锁定。
A locking read clause in an outer statement does not lock the rows of a table in a nested subquery unless a locking read clause is also specified in the subquery. refer
应用层面的悲观锁
如果有多个分布式的应用需要与数据库产生并发操作,那么我们可以引入一个第三方协调服务来处理并发问题,其本质还是一种悲观锁的实现。
例如: 基于zookeeper的分布式锁