数据库事务
数据库事务定义,满足4个特性:原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称ACID。
原子性:表示组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作都执行成功,整个事务才提交,事务中任何一个数据库操作失败,已经执行的任何操作都必须撤销,让数据库返回到初始状态。
一致性:事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏。如从A账户转账100元到B账户,不管操作成功与否,A和B的存款总额是不变的。
隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。准确地说,并非要求做到完全无干扰,数据库规了多种事务隔离级别,不同隔离级别对应不同的干扰程序,隔离级别越高,数据一致性越好,但并发性越弱。
持久性:一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须保证能够通过某种机制恢复数据。
数据库并发的问题
一个数据库可能拥有多个访问客户端,这些客户端都可用并发的方式访问数据库。数据库中的相同数据可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。这些问题可以归结为5类,包括3类数据读问题(脏读、不可重复读、幻象读)和2类数据更新问题(第一类丢失更新和第二类丢失更新)
脏读(dirty read)
A事务读取B事务尚未提交的数据,并在这个数据的基础上操作。如果恰巧B事务回滚,那么A事务读到的数据根本是不被承认的。
时间 | 转账事务A | 取款事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 取出500元,把余额修改为500元 | |
T5 | 查询账户余额为500元(脏读) | |
T6 | 撤销事务余额恢复为1000元 | |
T7 | 汇入100元把余额改为600元 | |
T8 | 提交事务 |
不可重复读(unrepeatable read)
不可重复读是指A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取的账户的余额发生不一致:
时间 | 取款事务A | 取款事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出100元把余额修改为900元 | |
T6 | 提交事务 | |
T7 | 查询账户余额为900元,与T4读取的不一致 |
幻象读(phantom read)
A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题。幻象读一般发生在计算统计数据的事务中
时间 | 统计金额事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计总存款数为1000元 | |
T4 | 新增一个存款账户,存款为100元 | |
T5 | 提交事务 | |
T6 | 再次统计总存款数为1100元,幻象读 |
幻象读和不可重复读是两个容易混淆的概念,幻象读是指读到了其他已经提交事务的新增数据,不可重复读是指读到了已经提交事务的更改数据(更改或删除),为了避免这两种情况,采取的对策是不同的,防止读取到更改数据,只需要对操作的数据添加行级锁,阻止操作中的数据发生变化,而防止读取到新增加的数据,则往往需要添加表级锁——将整个表锁定,防止新增数据。
第一类丢失更新
A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能千万很严重的问题
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 汇入100元,把余额修改为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元,把余额改为900元 | |
T8 | 撤销事务 | |
T9 | 余额恢复为1000元(丢失更新) |
第二类丢失更新
A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 汇入100元,把余额修改为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元,把余额改为900元 | |
T8 | 提交事务(丢失更新) |
ThreadLocal,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
ThreadLocal是如何做到为每一个线程维护一份独立的变量副本呢?思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。
数据库锁机制
按锁定的对象的不同,一般可以分为表锁定和行锁定,前者对整个表进行锁定,而后者对表中特定的行进行锁定。从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定既防止其他的独占锁定,也防止其他的共享锁定。为了更改数据,数据库必须在进行更改的行上施加行独占锁定。
事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻象读 | 第一类丢失更新 | 第二类丢失更新 |
---|---|---|---|---|---|
READ UNCOMMITED | 允许 | 允许 | 允许 | 不允许 | 允许 |
READ COMMITED | 不允许 | 允许 | 允许 | 不允许 | 允许 |
REPEATABLE READ | 不允许 | 不允许 | 允许 | 不允许 | 不允许 |
SERIALIZABLE | 不允许 | 不允许 | 不允许 | 不允许 | 不允许 |