为什么需要锁?
因为数据库要解决并发控制问题。在同一时刻,可能会有多个客户端对Table1.rown进行操作,比如有的在读取该行数据,其他的尝试去删除它。为了保证数据的一致性,数据库就要对这种并发操作进行控制,因此就有了锁的概念。
锁的分类
从对数据操作的类型(读\写)分
读锁(共享锁):针对同一块数据,多个读操作可以同时进行而不会互相影响。
写锁(排他锁):当当前写操作没有完成前,它会阻断其他写锁和读锁。
从锁定的数据范围分
表锁
行锁
为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取,检查,释放锁等动作),因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度(Lock granularity)”的概念
锁粒度(Lock granularity)
表锁:管理锁的开销最小,同时允许的并发量也最小的锁机制。MyIsam存储引擎使用的锁机制。当要写入数据时,把整个表都锁上,此时其他读、写动作一律等待。在MySql中,除了MyIsam存储引擎使用这种锁策略外,MySql本身也使用表锁来执行某些特定动作,比如alter table.
行锁:可以支持最大并发的锁策略。InnoDB和Falcon两张存储引擎都采用这种策略。
MySql是一种开放的架构,你可以实现自己的存储引擎,并实现自己的锁粒度策略,不像Oracle,你没有机会改变锁策略,Oracle采用的是行锁。
事务(Transaction)
从业务角度出发,对数据库的一组操作要求保持4个特征:
Atomicity:原子性
Consistency:一致性,
Isolation:隔离性
Durability:持久性
为了更好地理解ACID,以银行账户转账为例:
1 START TRANSACTION;
2 SELECT balance FROM checking WHERE customer_id = 10233276;
3 UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
4 UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
5 COMMIT;
原子性:要么完全提交(10233276的checking余额减少200,savings 的余额增加200),要么完全回滚(两个表的余额都不发生变化)
一致性:这个例子的一致性体现在 200元不会因为数据库系统运行到第3行之后,第4行之前时崩溃而不翼而飞,因为事物还没有提交。
隔离性:允许在一个事务中的操作语句会与其他事务的语句隔离开,比如事务A运行到第3行之后,第4行之前,此时事务B去查询checking余额时,它仍然能够看到在事务A中被减去的200元,因为事务A和B是彼此隔离的。在事务A提交之前,事务B观察不到数据的改变。
持久性:这个很好理解。
事务跟锁一样都会需要大量工作,因此你可以根据你自己的需要来决定是否需要事务支持,从而选择不同的存储引擎。
隔离级别(Isolation Level)
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。-----针对未提交事物或回滚事物。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。-----针对已提交的更新或删除操作。
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。-----针对已提交的插入操作。
readonly详解
1. 首先最根本的,我们要看一下数据库中对于readonly事务的定义!
第一个帖子中已经给出了结论:
- Oracle默认情况下保证了SQL语句级别的读一致性,即在该条SQL语句执行期间,它只会看到执行前点的数据状态,而不会看到执行期间数据被其他SQL改变的状态。
- 而Oracle的只读查询(read-only transaction)则保证了事务级别的读一致性,即在该事务范围内执行的多条SQL都只会看到执行前点的数据状态,
- 而不会看到事务期间的任何被其他SQL改变的状态。
这个是Oracle对于只读事务的描述,要保证事务级别的读一致性有两种方式,第一个帖子中也给出了结论:
- 一是用SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
- 二是用SET TRANSCATION READ ONLY
我们来做一下测试(采用第二种方式):
编写一个oracle的存储过程:
- create or replace procedure p_test as
- ware_code varchar2(14);
- begin
- set transaction read only;
- select t.ware_code into ware_code from tp_area t where t.area_code = '1';
- dbms_output.put_line(ware_code);
- dbms_lock.sleep(10);--暂停10秒
- select t.ware_code into ware_code from tp_area t where t.area_code = '1';
- dbms_output.put_line(ware_code);
- end p_test;
area_code是tp_area表的主键。在暂停的10秒钟内,我们去更改area_code为1的这条记录中ware_code的值,并提交。
经过测试发现,两次的输出结果一致,这就证明了正确性。
ps:对于oracle中只读事务的描述,推荐参考《Oracle 9i&10g编程艺术:深入数据库体系结构》这本书!
但是我们要说明,并不是所有的数据库都支持readonly事务。
2. 接下来我们讨论jdbc中对于readonly事务的描述
jdk1.6中java.sql.Connection接口中定义的方法
setReadOnly(boolean readOnly) throws SQLException的描述:将此连接设置为只读模式,作为驱动程序启用数据库优化的提示。
例如:此连接设置为只读模式后,通知Oracle后,Oracle做自己的优化;通知DB2后,DB2做自己的优化等等,但是并不一定对于数据库而言这就是readonly事务,此readonly并非彼readonly!
讲到这里,很多人可能并不信,难道setReadOnly为true之后数据库并不是以readonly事务执行的?
我们以实际的测试结果说话:
测试环境一:
- 数据库:Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
- oracle jdbc驱动版本:Oracle JDBC Driver version - 9.0.2.0.0 此驱动已经是很老的了,但是相信还是有很多项目还在用就是ojdbc14.jar。
- jdk版本:jdk 1.6.0_20
测试代码:
- Class.forName("oracle.jdbc.OracleDriver");
- Connection conn = DriverManager.getConnection(url, username, password);
- conn.setAutoCommit(false);
- conn.setReadOnly(true);//启动只读模式
- PreparedStatement ps = conn.prepareStatement("select s.ware_code from tp_area s where s.area_code = '1'");
- ResultSet rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- Thread.sleep(10000);//暂停10秒
- ps = conn.prepareStatement("select s.ware_code from tp_area s where s.area_code = '1'");
- rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- ps.close();
- conn.commit();
在暂定的10秒钟内,我通过pl/sql手动去修改了这条记录中ware_code的值,并提交,那这两次的输出结果一样吗?
测试的结果是:这两次的输出结果是一致的!
咦,这不是对的吗?oracle启动了只读事务,根据事务级都一致性的原则,两次读出来的应该是同一时间点的数据,应该一致啊!
好,别着急,我们再来看一个测试。
测试环境二:
- 数据库:Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
- oracle jdbc驱动版本:Oracle JDBC Driver version - 11.2.0.1.0 这种从官方网站上面下载的最新的驱动ojdbc5.jar
- jdk版本:jdk 1.6.0_20
测试代码(与上面的测试代码一致):
- Class.forName("oracle.jdbc.OracleDriver");
- Connection conn = DriverManager.getConnection(url, username, password);
- conn.setAutoCommit(false);
- conn.setReadOnly(true);
- PreparedStatement ps = conn.prepareStatement("select s.ware_code from tp_area s where s.area_code = '1'");
- ResultSet rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- Thread.sleep(10000);//暂停10秒
- ps = conn.prepareStatement("select s.ware_code from tp_area s where s.area_code = '1'");
- rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- ps.close();
- conn.commit();
在暂定的10秒钟内,我同样还是通过pl/sql手动去修改了这条记录中ware_code的值,那这两次的输出结果一样吗?
测试的结果是:这两次的输出结果是不一致的!
--------------------------------------------------------------------分割线--------------------------------------------------------------------------------
是不是不太敢相信自己的眼睛,我开始的时候也是很纳闷,为啥会这样!!??
我们先来看一下 Oracle JDBC Driver version - 9.0.2.0.0 版本的的api中的说明:
- oracle.jdbc.driver.OracleConnection
- public void setReadOnly(boolean value) throws java.sql.SQLException
- Sets the Connection as read only if the value is true and the enables the Connection for writing (updat/delete/insert) with the value as false.
但是最直接有效的方式还是看源码!
好的,我们来看一下Oracle JDBC Driver version - 9.0.2.0.0 ojdbc14.jar这个版本的驱动源码:
- <span>//</span><span style="white-space: normal;">jdbc.oracle.driver.OracleConnection 的代码片段</span><span>
- public void setReadOnly(boolean flag)
- throws SQLException
- {
- PreparedStatement preparedstatement = null;
- try
- {
- String s = null;
- if (flag)
- s = "SET TRANSACTION READ ONLY";
- else
- s = "SET TRANSACTION READ WRITE";
- preparedstatement = prepareStatement(s);
- preparedstatement.execute();
- }
- finally
- {
- if (preparedstatement != null)
- preparedstatement.close();
- }
- m_readOnly = flag;
- }
- </span>
从源码中我们可以很明显的看出来当setReadOnly为true时,究竟做了什么事-->"SET TRANSACTION READ ONLY",
所以在此版本的jdbc实现中只要你setReadOnly为true,则对于数据库而言就是以只读事务来执行。
那Oracle JDBC Driver version - 11.2.0.1.0这个版本的驱动究竟怎么回事呢?
首先从这个版本的api中,我们已经找不到oracle.jdbc.driver.OracleConnection这个类了,
我们来看oracle.jdbc.OracleConnectionWrapper implements oracle.jdbc.OracleConnection
虽然api中存在public void setReadOnly(boolean readOnly) throws java.sql.SQLException这个方法,但是没有任何描述,很奇怪。
我们来看一下源码的片段:
- protected oracle.jdbc.OracleConnection connection;
- .....
- public void setReadOnly(boolean flag)
- throws SQLException
- {
- connection.setReadOnly(flag);
- }
- //OracleConnection接口声明:public interface OracleConnection extends Connection
你能够看出什么来吗? 是啊,根本就没做什么是嘛,对的,什么事情都没做!
ps:貌似此版本的驱动对于readonly属性就是抛弃了,没起作用!(没有再做深入研究,不知这样讲是否正确)
甚至说我如下的代码都可以执行通过:
- Class.forName("oracle.jdbc.OracleDriver");
- Connection conn = DriverManager.getConnection(url, username, password);
- conn.setAutoCommit(false);
- conn.setReadOnly(true);
- PreparedStatement ps = conn.prepareStatement("update tp_area t set t.ware_code ='t'");
- ps.executeUpdate();
- ps.close();
- conn.commit();
设置只读后,我还能执行更新操作,并且运行测试都是Ok的!
但是如果我想要实现oracle的readonly事务该怎么办呢?
你可以从Oracle JDBC Driver version - 9.0.2.0.0 ojdbc14.jar源码中得到思路,代码如下:
- Class.forName("oracle.jdbc.OracleDriver");
- Connection conn = DriverManager.getConnection(url, username, password);
- conn.setAutoCommit(false);
- conn.setReadOnly(true);
- //新增的两行
- PreparedStatement ps = conn.prepareStatement("set transaction read only");
- ps.execute();
- ps = conn.prepareStatement("select s.ware_code from tp_area s where s.area_code = '1'");
- ResultSet rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- Thread.sleep(10000);//暂停10秒
- ps = conn.prepareStatement("select s.ware_code from tp_area s where s.area_code = '1'");
- rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- ps.close();
- conn.commit();
对的,就是设置手动显示地 "set transaction read only"即可!ps:不知道还有没有其他的方式?
经过上述分析后是不是豁然开朗的许多!
总结:1. 首先jdbc的规范中已经说明了readonly只是将此连接设置为只读模式,作为驱动程序启用数据库优化的提示,并不一定以只读事务执行!
2. 对于oracle的jdbc驱动而言,不同版本的驱动会得出不同的结论!
但是我不清楚的是为什么oracle在后续驱动中,不支持了readonly事务了呢?处于性能的考虑?还是别的其他原因,有待求证!
3. SSH架构中Spring的readonly事务
底层的驱动都可能不支持readonly事务,你说Spring的readonly事务能管用吗?答案是很显然的,当然不一定支持。
第二个帖子中的theone说的是对的。
- “只读事务”并不是一个强制选项,它只是一个“暗示”,提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,
- 那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。