一、事务的介绍
1、什么是事务
事务是数据库操作的最基本单元,是逻辑上的一组操作。这组操作的结果要么成功,要么失败。以取钱为例来说明事务:如果小明去ATM机取200块钱,正常结果就是银行卡扣掉200块钱,ATM出200块钱,然后小明拿走。这两个步骤可以整体看作一个事务,并且要么都执行,要么都不执行。如果银行扣掉200块钱,而ATM故障无法出钱,这对客户来说是无法容忍的。如果银行扣钱失败但ATM出了200被小明拿走,这对银行来说就是损失。为了不让双方都有损失,最好的方法就是上述两个步骤要么都执行,要么都是不执行,如果任何一方出现故障或失败,则整个取钱的过程就会回滚,回到最初的状态。这里就该事务登场来解决此类问题。
2、事务的特性——ACID
(1)原子性(Atomicity):事务是由一系列动作组成的原子操作,原子是不可再分的。事务的原子性确保了动作的执行结果要么全部完成,要么全部不完成。
(2)一致性(Consistency):事务的一致性是指在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。比如说tom和jerry各有500块钱,他们加在一起就是1000块钱,现在tom给jerry转账100,此时tom就有400,而Jerry有600,但他们加在一起还是1000。
(3)隔离性(Isolation):在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
(4)持久性(Durability):只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态
3、并发场景下事务产生的问题
(1)脏读:脏读是指事务A读取到了事务B还没有提交的数据。假设有1000块钱,事务A开启事务,此时事务B开启事务并从中取出100块钱,但没有提交。如果此时事务A读取数据,那么得到的是1000,这就是所谓的脏读。
(2)不可重复读:不可重复读是指在一个事务里多次读取同一个数据,但是得到的数据结果不一致。假设有1000块钱,事务A开启事务,得到结果为1000,此时事务B开启事务并从中取出100块钱并提交,事务A再次读取,就变成了900。也就是说,事务B在事务A多次读取的过程中,对数据进行更新提交,导致事务A多次读取得到的结果不一致。
(3)幻读:幻读是指在一个事务里面的操作中出现了未被操作的数据。当事务A在读取数据时,事务B插入了数据,导致事务A第二次读取数据的时候数据不一致,如同发生幻觉一般。
4、Spring中的事务的隔离级别
事务隔离级别是为了解决上面几种问题而诞生的。事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大,因此很多时候必须在并发性和性能之间做一个权衡。所以要根据自己项目的并发情况选择合适的事务隔离级别
事务隔离级别有4种,Spring会提供给用户5种:
(1)DEFAULT
默认隔离级别,每种数据库支持的事务隔离级别不一样,如果Spring配置事务时将isolation设置为这个值的话,那么将使用底层数据库的默认事务隔离级别。MySQL使用"select @@tx_isolation"来查看默认的事务隔离级别。
(2)READ_UNCOMMITTED
读未提交,即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用。
(3)READ_COMMITED
读已提交,即能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读。
(4)REPEATABLE_READ
重复读取,即在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决。
(5)SERLALIZABLE
串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了
如下表:
隔离级别 | 脏读可能性 | 不可重复读可能性 | 幻读可能性 | 加锁读 |
READ_UNCOMMITTED | 是 | 是 | 是 | 否 |
READ_COMMITTED | 否 | 是 | 是 | 否 |
REPEATABLE_READ | 否 | 否 | 是 | 否 |
SERIALIZABLE | 否 | 否 | 否 | 是 |
注意:大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read。
二、简单案例——转账
(1)创建外部配置文件jdbc.properties
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.userName=root
jdbc.password=root
(2)创建bean.xml,配置数据源、jdbcTemplate以及注解扫描的包。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--引入context名称空间-->
<!-- 配置注解扫描的包 -->
<context:component-scan base-package="com.yht.example8"></context:component-scan>
<!--引入外部属性文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClass}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.userName}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 配置jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
(3)创建t_account表并插入数据。
create table t_account(
name varchar(20),
money int);
insert into t_account values("tom", 1000);
insert into t_account values("jerry", 1000);
(4)创建UserService类
@Service
public class UserService {
@Autowired
private IUserDao userDao;
public void tranferAccount(){
userDao.reduceMoney();
userDao.addMoney();
}
}
(5)创建IUserDao和UserDaoImpl类
public interface IUserDao {
void reduceMoney();
void addMoney();
}
@Repository
public class UserDaoImpl implements IUserDao {
@Autowired
public JdbcTemplate jdbcTemplate;
@Override
public void reduceMoney() {
String sql = "update t_account set money=money-? where name=?";
jdbcTemplate.update(sql, 100, "tom");
}
@Override
public void addMoney() {
String sql = "update t_account set money=money+? where name=?";
jdbcTemplate.update(sql, 100, "jerry");
}
}
(6)进行单元测试。
@Test
public void testAccount(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.tranferAccount();
}
说明:以上代码是一个正常转账的流程,最终也能达到预期的效果。但是,如果在UserService的tranferAccount()中转账的两个步骤之间出现异常,那么转账是否会成功呢?
转账之前数据库中的信息:
在tranferAccount()模拟一个异常:
public void tranferAccount(){
userDao.reduceMoney();
int i = Integer.parseInt("123a");
userDao.addMoney();
}
此时数据库中的结果:
从结果中可以看出转账失败了,tom少了100,但是jerry的money却没有增加,这在实际生活中是不被允许的。在接下来的文章中,将通过Spring中的事务管理操作来解决该问题。