Mybatis的事务管理
1、事务的概念
在我们开发过程中,几乎每个业务逻辑都离不开对数据库的操作。那么对数据的的单个操作(单个CRUD)或者多个操作(多个CRUD)绑定在一起,就称为事务。
单个事务是一个最小的逻辑执行单元,整个事务不能分开执行,要么同时成功,要么同时失败。(PS:初次接触事务概念的朋友,也许会难以理解最小逻辑执行单元,反正当初我是误解了,误解的地方还真有点描述不出来,原谅我词穷,下面举个例子领悟吧)。
场景一:
在我们的service层(业务逻辑层)要执行一个转账操作(A账户转账到B账户,转100元)。那么简单的做法就应该是A的账户执行一个update操作减去100,然后B的账户也执行一个update操作增加100。这两个操作绑定到一起,就称为事务。这个事务是不能分开执行的,要么同时成功(转账成功),要么同时失败(转账失败)。不能A账户少了100,B账户没增加100,那这100归我(哈哈)?这样就破坏了数据的完整性了。
场景二:
将一批数据插入到数据库中,如果要求同时成功或者同时失败,当然是不可靠的,假如每次在执行过程中程序都出现意外呢(^_^),那么就需要将每次插入(insert操作)作为一个事务(这也说明,多个事务是可以同时存在的,这也是容易误解的地方)。(反例:当然,我们将所有的insert操作绑定在一起也是一个事务,那么就要求,要么全部失败,要么全部成功,是不能分开执行的)。
这两个场景在我们的业务逻辑层具体怎么实现,体现的就是事务的传播行为(后面我们单独开一篇来讲事务)。
通常来讲,事务具备4个特性,就是经常听人提起的(ACID):原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。具体说明见下表:
特性 | 说明 |
原子性(Atomicity) | 也就是说事务是应用中的最小逻辑执行单元,就像自然界中的原子一样,不能再被拆分,也就是再次强调,单个事务是不能够分开执行的(哪怕你单个事务中包含了n多个CRUD,都是要么全部成功,要么全部失败)。 |
一致性(Consistency) | 事务的执行结果,必须使数据库从一种一致性状态变成另一种一致性状态。就比如上面的场景一,A、B之间不管转账几次,A、B的账户总额一定是不变的,A、B的账户和总额之间的关系始终是一致的。如果A减少100,B没增加,那么它们之间的一致性就被破坏了。因此,一致性是通过原子性保证的。 |
隔离性(Isolation) | 各个事务之间的执行是互不干扰的,比如上面的场景二,单个事务的insert插入,并不不能影响下一个事务的insert操作成功与否,再比如我和你同时访问CSDN(文章评论的业务逻辑肯定只有一个),我对一篇文章的评论能否提交成功,并不能影响你对一篇文章的评论能否提交成功。所以,事务也是并发的。 |
持久性(Durability) | 我们单个事务一旦提交,那么对数据所做的操作都是永远保存在数据库中的,直到下一个事务再对其进行更改 |
那么,在我们去实现事务的过程中,就应该包含以下几个操作:创建(create)、提交(commit)、回滚(rollback)、关闭(close)。
回到我们上面的场景一就是:创建一个事务分别包含A和B的update操作,如果两个update操作都成功了,则commit,如果在两个update执行过程中,断网或者断电了,再或者B被匪徒劫持了导致A取消转账,则rollback,最后close事务。
2、Mybatis的事务管理
在Mybatis中,事务的管理分为两种形式:
>使用JDBC的事务管理机制: 就是在没使用ORM框架之前,基于jdbc中java.sql.Connection对象里面的commit()、rollback()、close()方法实现。
>使用MANAGED的事务管理机制。对于这种机制,Mybatis自身不会去实现对事务的管理,而是让容器去实现,比如我们常用的Spring。那么,我们在使用Spring+Mybatis整合的开发方式时,如果将事务交给Spring去管理,就应该配置Mybatis为MANAGED的事务机制。当然,现在一般都是整合开发,所以都是选择这种方式。
3、事务的配置创建和简单使用
<environments default="mysql">
<environment id="mysql">
<!-- 指定事务管理类型,指使用JDBC的提交和回滚设置 -->
<transactionManager type="JDBC"/>
</environments>
就是在我们的Mybatis配置文件中(传送门:配置文件详解)直接定义如上的信息即可。这里只用了Mybais,所以选择了JDBC的事务管理机制。
剩下的一系列工作,Mybatis都提供了事务工厂(TransactionFactory)为我们创建并管理事务,我们只需要调用方法即可。(这里不分析Mybatis是怎么在操作,可以直接点开TransactionFactory或者在运行过程中debug,只做简单的操作示例,篇幅有限,水平也有限,见谅哦^_^)。
接下来,我们就可以愉快的玩耍了。
创建如下的表并配置好环境(参考:Mybatis第一篇)
public class Test {
public static void main(String[] args) {
InputStream is = Test.class.getResourceAsStream("/mybatis/mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
//注意这里-----
SqlSession sqlSession = sqlSessionFactory.openSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
//插入两条数据
User user1 = new User();
user1.setUserName("孙悟空");
user1.setUserAge(1000);
userDao.saveUser(user1);
User user2 = new User();
user2.setUserName("猪八戒");
user2.setUserAge(500);
userDao.saveUser(user2);
try {
int i = 5/0;//假如这里执行其他业务逻辑产生了异常行为
} catch (Exception e) {
sqlSession.rollback();//回滚
}
sqlSession.close();
}
}
执行上面的测试代码,会发现,数据库中并没有数据的插入,因为被我们执行回滚了。
注意这里:
SqlSession sqlSession = sqlSessionFactory.openSession();
在获取SqlSession对象的时候,我们可以对其进行一系列的设置。
1、开启批处理(上一篇已经深入了解过);
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
2、设置隔离级别(这个需要底层数据库的支持)
SqlSession sqlSession = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
3、设置自动提交(commit)
将上面的测试代码,删掉rollback(),不执行commit(),数据库同样会没数据,需要手动执行commit(),假如设置自动提交,就算不执行commit()也会提交。
SqlSession sqlSession = sqlSessionFactory.openSession(true);
还有很多其它的设置,需要我们去发现
Mybatis的缓存机制
缓存的机制就是,将我们的数据置于内存中,然后我们就可以直接在内存中访问数据,省去了对底层数据库的操作(因为数据库的数据置于硬盘上,所以访问数据需要IO的操作,还有和数据库建立连接等等)。
在现实的互联网产品中,很大部分都是查询功能占主比。所以,通常对数据库查询的性能要求很高。而Mybatis则提供了这样的功能来为我们提高查询性能。
Mybatis的缓存分为一级缓存和二级缓存。一级缓存是SqlSession级别的,也就是只存在于我们当前的SqlSession作用域中,简单的比喻就是,比如两个用户通过Mybatis访问我们的数据,Mybatis则会为两个用户创建不同的SqlSession,那么A的一级缓存,则不能被B共享。二级缓存是Mapper级别的,可以简单理解为,二级缓存存在于Mybatis处理Mapper文件的功能中,是可以多个SqlSession共享的。
1、一级缓存
一级缓存的作用域是SqlSession范围的。实现的原理大致就是,我们构建SqlSession之后,会在SqlSession的作用域中创建一个HashMap用于缓存数据,这个很好理解,就是我们在平时一些编码过程中,也会经常使用HashMap作为数据的容器,但是它会随着当前对象的作用域创建而创建,也会随着当前对象作用域被销毁而销毁。
需要注意的是,如果当前SqlSession执行了DML语句(insert、update、delete),并提交到了数据库,则Mybatis会情况SqlSession中的一级缓存,这样做的目的是为了保证缓存中存储的是最新的信息,避免出现脏读现象(比如A的账户有100元,然后执行了update变为50元,再次查询因为缓存没清空的原因还是100元,这就是脏读现象)。当一个SqlSession关闭后,当前SqlSession中的缓存也是不存在的。
Mybatis是默认存在一级缓存的,不需要进行任何配置,也不能被关闭,所以直接上示例吧。同样是user表
public class Test {
public static void main(String[] args) {
InputStream is = Test.class.getResourceAsStream("/mybatis/mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
User user = userDao.getUserById(1);//查询
System.out.println(user.toString());
User user2 = userDao.getUserById(1);
System.out.println(user2.toString());//再次查询
sqlSession.close();
}
}
DEBUG [main] - ==> Preparing: SELECT * FROM tb_user WHERE user_id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [userId=1, userName=张三, userAge=20]
User [userId=1, userName=张三, userAge=20]
可以看到只执行了一条SQL语句。关闭SqlSession从新获取SqlSession或者再次执行、或者在两次查询中间执行DML操作,都会清空掉缓存。
比如下面是在两次查询中间假如了一个插入语句后执行的结果
DEBUG [main] - ==> Preparing: SELECT * FROM tb_user WHERE user_id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [userId=1, userName=张三, userAge=20]
DEBUG [main] - ==> Preparing: INSERT INTO tb_user (user_name, user_age) VALUEs (?, ?);
DEBUG [main] - ==> Parameters: ...(String), 12(Integer)
DEBUG [main] - ==> Preparing: SELECT * FROM tb_user WHERE user_id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [userId=1, userName=张三, userAge=20]
很明显执行了两条查询的SQL,中间穿插了一条插入SQL。
2、二级缓存
二级缓存是mapper级别的。与一级缓存不同的是,可以多个SqlSession作用域共享。需要注意的是,使用二级缓存时,需要映射对象实现序列化接口java.io.Serializable,如果存在父类,其每个成员都需要实现序列化接口。二级缓存并不会因为执行了DML操作而更新或失效,所以需要手动设置一些属性,保证数据的实时性,从而最小化的避免脏读现象。
使用二级缓存的步骤
首先,在mybatis的配置文件中,开启二级缓存,该属性配置默认为false(参考传送门:详解Mybatis配置文件)
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
然后在需要开启二级缓存的mapper作用域下开启开启二级缓存。即在mapper映射文件下:
<mapper namespace="com.zepal.mybatis.dao.UserDao">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
</mapper>
也可以在<select>标签上单独为某一个查询开启二级缓存(如下,其它属性配置参考详解mapper文件),如果设置为false,则不会为当前查询开启二级缓存,优先级是高于全局的。
<select useCache="true" id=""></select>
这样就成功创建二级缓存了,cache标签用来开启当前mapper的命名空间(namespace)下的二级缓存。
cache标签的详细属性如下:
属性 | 说明 |
flushInterval | 缓存刷新间隔。可以使任意的正整数,单位是毫秒(ms)。没有默认值,如果不设置,则没有刷新间隔 |
size | 缓存数目。可以设置为任意正整数(注意你的计算机内存容量哦),默认是1024 |
readOnly | 是否只读。属性可以被设置为true或false,默认false,只读的缓存会给所有的调用者返回缓存对象的相同实例,因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。 |
eviction | 收回策略,默认为LRU。有如下几种:LRU-最近最少使用策略,移除最长时间不被使用的对象。FIFO-先进先出策略,按对象进行缓存的顺序来移除它们。SOFT-软引用策略,移除基于垃圾回收器状态和软引用规则的对象。WEAK-弱引用策略,更积极的移除基于垃圾回收器状态和弱引用规则的对象。 |
接下来二级缓存的测试就可以和一级缓存一样各种尝试各种进行了。
总结
一个系统的是否健壮,一定要将事务考虑周全,但是直接使用Mybais的JDBC的事务管理机制,总是不那么方便,而且现在几乎都是基于Spring在开发,所以常常使用Mybatis的MANAGED事务管理机制,将事务管理交给其它容器。
缓存对我们的系统性能提升很大,但是Mybatis的缓存机制仅可作为辅助手段,不能作为主要缓存手段(现在几乎都是Redis、Memcached或者Guava Cache的本地缓存),,撇开性能的因素,管理起来也方便得多,Mybatis的二级缓存使用起来,不能手动更新是一大弊端,只能通过设置一些参数,让其自动更新,那么出现缓存脏读的几率就很大。
此篇完结