数据库事务基础知识
数据库事务是什么
事务要么整体生效,要么整体失效。在数据库上即多条SQL语句要么全执行成功,要么全执行失败
数据库事务必须同时满足4个特性:
原子性(Atomic),一致性(Consistency),隔离性(Isolation)和持久性(Durabiliy)
一致性是最终目标,其他特性都是为了达到这个目标而采取的措施。
数据库管理系统一般采用重执行日志来确保原子性,一致性和持久性。
重执行日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库即可根据重执行日志撤销已经执行的操作。对于已经提交的事务,即使数据库奔溃,在重启数据库时也能根据日志对尚未持久化的数据进行相应的重执行操作
数据库管理系统采用数据库锁机制保证事务的隔离性。当多个事务视图对相同的数据进行操作时,只有持有锁的事务才能操作数据,直到前一个事务完成后,后面的事务才有机会对数据进行操作。
Oracle数据库使用了数据版本的机制,为回滚段为数据的每个变化都保存一个版本,使数据的更改不影响数据的读取
数据并发的问题
一个数据库可能拥有多个访问客户端,这些客户端都可用并发的方式访问数据库。数据库中的相同数据被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。
这些问题可以归为5类,包括3类数据读问题(脏读,不可重复读和幻象读)及2类数据更新问题(第一类丢失更新和第二类丢失更新)
1.脏读(dirty read)
A事务读取B事务尚未提交的更改数据,并在这个数据的基础上进行操作。如果恰巧B事务回滚,那么A事务读到的数据根本是不被承认的
因为A事务读取了事务尚未提交的数据,因而造成了丢了500元。
Oracle数据库中,不会发生脏读。
2.不可重复读(unrepeatable read)
不可重复读是指A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两个读取账户的余额发生不一致。
在同一事务中,T4时间点和T7时间点读取的账户存款余额不一致
3.幻象读(phantom read)
A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题,幻象读一般发生在计算统计数据的事务中
举例,假设银行系统在同一个事务中两次统计存款账户的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100,这时两次统计的总金额将不一致
幻象读是指读到了其他已经提交的新增数据,而不可重复读是指读到了已经提交事务的更改数据(更改或删除)
防止读到更改数据,只需对操作的数据添加行级锁,阻止操作中的数据发生变化
防止读到新增数据,需要添加表级锁——将整张表锁定,防止新增数据(Oracle使用多版本数据的方式实现)
4.第一类丢失更新
A事务撤销时,把已经提交的B事务的更新数据覆盖了。
A事务在撤销时,将B事务已经转入账户的金额抹去了
5.第二类丢失更新
A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失
由于转账事务覆盖了取款事务对存款余额所做的更新,导致银行损失了100.
如果转账事务先提交,用户账户损失100
数据库锁机制
数据库通过锁机制解决并发访问的问题。
按锁定的对象的不同,分为表锁定和行锁定。
表锁定对整张表进行锁定,行锁定对表中的特定行进行锁定。
从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。
共享锁定会防止独占锁定,但允许其他的共享锁定,独占锁定既防止其他的独占锁定,也防止其他的共享锁定。
为了更改数据,数据库必须在更改的行上施加行独占锁定,INSERT,UPDATE,DELELTE和SELECT FOR UPDATE语句都会隐式采用必要的行锁定。
下面是Oracle数据库常用的5种锁定
事务隔离级别
直接使用锁管理是非常麻烦的,因此数据库为用户提供了自动锁机制。
只要用户指定会话的事务隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加合适的锁 。此外,数据库还会维护这些锁,当一个资源的锁数目太多时,自动进行锁升级以提高系统的运行性能,而这一过程对用户来说是透明的。
ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别
事务的隔离级别和数据库并发性是对立的。一般来说,使用READ UNCOMMITED隔离级别的数据库拥有最高的并发性和吞吐量,而使用SERIALIZABLE隔离级别的数据库并发性最低。
JDBC对事务的支持
用户可以通过Connection#getMetaData()方法获取DatabaseMetaData对象,并通过该对象的supportsTransactions(),supportsTransactionIsolationLevel(int level)方法查看底层数据库的事务支持情况。
Connection默认情况下是自动提交的,即每条执行的SQL语句都对应一个事务。为了将多条SQL语句当成一个事务执行,必须先通过Connection#setAutoCommit(false)阻止Connection自动提交,并通过Connection#setTransactionIsolation()设置事务的隔离级别。
Connection中定义了对应SQL 92标准的4个事务隔离级别的常量。通过Connection#commit()提交事务,通过Connection#rollback()回滚事务。
JDBC 3.0引入了一个全新的保存点特性,Savepoint接口允许用户将事务分割为多个阶段,用户可以指定回滚到事务的特定保存点,而并非只能回滚到开始事务的点
在发生特定问题时,回滚到指定的保存点,而非回滚整个事务
用户可以通过DatabaseMetaData#supportsSavepoints()方法查看是否支持保存点功能
ThreadLocal(线程封闭)
ThreadLocal,是保存线程本地化对象的容器。当运行于多线程环境中的某个对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。
所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程角度看,这个变量就像线程专有的本地变量
InheritableThreadLocal继承于ThreadLocal,它自动为子线程复制一份从父线程那里继承来的本地变量:在创建子线程时,子线程会接收所有可继承的线程本地变量的初始值。
当需要将本地线程变量自动传送给所有创建的子线程时,应使用InheritableThreadLocal
ThreadLocal的接口方法
只有4个方法
ThreadLocal支持泛型,该类的类名变成了ThreadLocal<T>。API方法分别调整为void set(T value),T get()和T initialValue()
ThreadLocal类中有一个Map,用于存储每个线程变量副本,Map中元素的键为线程对象,值为对应线程的变量副本。这样ThreadLocal就能为每个线程维护一份独立的变量副本
下面是一个简单的版本,帮助理解。
public class SimpleThreadLocal {
private Map valueMap= Collections.synchronizedMap(new HashMap());
public void set(Object newValue){
valueMap.put(Thread.currentThread(),newValue); //键为线程,值为本线程的变量副本
}
public Object get(){
Thread currentThread=Thread.currentThread();
Object o=valueMap.get(currentThread); //返回本线程对应的变量
if(o==null&&!valueMap.containsKey(currentThread)){ //如果在Map中不存在,则放到Map中保存
o=initialValue();
valueMap.put(currentThread,o);
}
return o;
}
public void remove(){
valueMap.remove(Thread.currentThread());
}
public Object initialValue(){
return null;
}
}
ThreadLocal实例
public class SequenceNumber {
//通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
public static ThreadLocal<Integer> seqNum=new ThreadLocal<Integer>(){
public Integer initialValue(){
return 0;
}
};
//获取下一个序列值
public int getNextNum(){
seqNum.set(seqNum.get()+1);
return seqNum.get();
}
private