一、理论储备
1.1 数据库事务
数据库事务有严格的定义,一个数据库事务是一个单一的工作单元操作序列,它必须同时满足4个特性:原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)、和持久性(Durability),简称ACID。
原子性:表示组成一个事务的多个数据库操作是一个不可分割的单元,只有整个事务操作成功整个事务才提交,否则所有操作回滚;
一致性:事务操作成功后,数据库状态和它的业务规则是一致的;
隔离性:并发数据操作时不同事务拥有各自的数据空间,它们的操作不会对对方产生干扰。数据库规定了多种数据隔离级别,隔离级别越高,数据一致性越好,并发性越低。下文将详细例举;
持久性:一旦事务提交成功后事务中所有的数据都必须被持久化到数据库中,不能因系统故障而从数据库中删除。
Spring 支持两种类型的事务管理:
1.编程式事务管理 :这意味着你在编程的帮助下有管理事务。这给了你极大的灵活性,但却很难维护。
2.声明式事务管理 :这意味着你从业务代码中分离事务管理。你仅仅使用注解或 XML 配置来管理事务。
声明式事务管理比编程式事务管理更可取,尽管它不如编程式事务管理灵活,但它允许你通过代码控制事务。但作为一种横切关注点,声明式事务管理可以使用 AOP 方法进行模块化。Spring 支持使用 Spring AOP 框架的声明式事务管理。
1.2 数据库并发问题
一个数据库可能拥有多个访问客户端,这些客户端都可以并发地访问数据库。如果没有采取必要的隔离措施,就会导致各种并发问题。
1、脏读
脏读指一个事务读取了另外一个事务未提交的数据。
假设A向B转帐100元,对应sql语句如下所示
1.update account set money=money+100 where name=’B’;
2.update account set money=money-100 where name=’A’;
当第1条sql执行完,第2条还没执行(A未提交时),如果此时B查询自己的帐户,就会发现自己多了100元钱。如果A等B走后再回滚,B就会损失100元。
2、不可重复读
不可重复读指在一个事务内读取表中的某一行数据,多次读取结果不同。
例如银行想查询A帐户余额,第一次查询A帐户为200元,此时A向帐户内存了100元并提交了,银行接着又进行了一次查询,此时A帐户为300元了。银行两次查询不一致,可能就会很困惑,不知道哪次查询是准的。
不可重复读和脏读的区别是,脏读是读取前一事务未提交的脏数据,不可重复读是重新读取了前一事务已提交的数据。
如银行程序需要将查询结果分别输出到电脑屏幕和写到文件中,结果在一个事务中针对输出的目的地,进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。
3、幻读
幻读是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。(一般出现在查询事务中)
如丙存款100元未提交,这时银行做报表统计account表中所有用户的总额为500元,然后丙提交了,这时银行再统计发现帐户为600元了,造成虚读同样会使银行不知所措,到底以哪个为准。
4、第一类丢失
A事务撤销时,把已经提交的B事务的更新数据覆盖。
时间 | 取款事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 汇入100元把余额改为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元把余额改为900元 | |
T8 | 撤销事务 | |
T9 | 余额恢复为1000 元(丢失更新) |
5、第二类丢失
A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失。
时间 | 取款事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 汇入100元把余额改为1100元 | |
T6 | 提交事务 | |
T7 | 汇入100元 | |
T8 | 提交事务 | |
T9 | 把余额改为1100 元(丢失更新) |
上面的例子里由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户账户将损失100元。
第二类丢失更新,实际上和不可重复读是同一种问题。
1.3 Spring事务隔离级别和传播特性
Spring事务管理的实现有许多细节,如果对整个接口框架有个大体了解会非常有利于我们理解事务,Spring事务管理高层抽象主要包括3个接口,Spring的事务主要是由他们共同完成的:
PlatformTransactionManager:事务管理器—主要用于平台相关事务的管理
TransactionDefinition: 事务定义信息(隔离、传播、超时、只读)—通过配置如何进行事务管理。
TransactionStatus:事务具体运行状态—事务管理过程中,每个时间点事务的状态信息
Spring事务管理涉及的接口的联系如下:
参考了这篇文章
在Spring中,声明式事务是用事务参数来定义的。一个事务参数就是对事务策略应该如何应用到某个方法的一段描述,一个事务参数共有5个方面组成:
1)传播行为
事务的第一个方面是传播行为。传播行为定义关于客户端和被调用方法的事务边界。Spring定义了7中传播行为。
传播行为 | 意义 |
PROPAGATION_MANDATORY | 表示该方法必须运行在一个事务中。如果当前没有事务正在发生,将抛出一个异常 |
PROPAGATION_NESTED | 表示如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于封装事务进行提交或回滚。如果封装事务不存在,行为就像PROPAGATION_REQUIRES一样。 |
PROPAGATION_NEVER | 表示当前的方法不应该在一个事务中运行。如果一个事务正在进行,则会抛出一个异常。 |
PROPAGATION_NOT_SUPPORTED | 表示该方法不应该在一个事务中运行。如果一个现有事务正在进行中,它将在该方法的运行期间被挂起。 |
PROPAGATION_SUPPORTS | 表示当前方法不需要事务性上下文,但是如果有一个事务已经在运行的话,它也可以在这个事务里运行。 |
PROPAGATION_REQUIRES_NEW | 表示当前方法必须在它自己的事务里运行。一个新的事务将被启动,而且如果有一个现有事务在运行的话,则将在这个方法运行期间被挂起。 |
PROPAGATION_REQUIRES | 表示当前方法必须在一个事务中运行。如果一个现有事务正在进行中,该方法将在那个事务中运行,否则就要开始一个新事务。 |
2)隔离级别
声明式事务的第二个方面是隔离级别。隔离级别定义一个事务可能受其他并发事务活动活动影响的程度。另一种考虑一个事务的隔离级别的方式,是把它想象为那个事务对于事物处理数据的自私程度。
在理想状态下,事务之间将完全隔离,从而可以防止这些问题发生。然而,完全隔离会影响性能,因为隔离经常牵扯到锁定在数据库中的记录(而且有时是锁定完整的数据表)。侵占性的锁定会阻碍并发,要求事务相互等待来完成工作。
考虑到完全隔离会影响性能,而且并不是所有应用程序都要求完全隔离,所以有时可以在事务隔离方面灵活处理。因此,就会有好几个隔离级别。
隔离级别 | 含义 |
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别。 |
ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的更改。可能导致脏读、幻影读或不可重复读。 |
ISOLATION_READ_COMMITTED | 允许从已经提交的并发事务读取。可防止脏读,但幻影读和不可重复读仍可能会发生。 |
ISOLATION_REPEATABLE_READ | 对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻影读仍可能发生。 |
ISOLATION_SERIALIZABLE | 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。 |
3)只读
声明式事务的第三个特性是它是否是一个只读事务。如果一个事务只对后端数据库执行读操作,那么该数据库就可能利用那个事务的只读特性,采取某些优化 措施。通过把一个事务声明为只读,可以给后端数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的, 因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。
4)事务超时
为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的下一个特性就是它的超时。
假设事务的运行时间变得格外的长,由于事务可能涉及对后端数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。
由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。
5)回滚规则
事务五边形的对后一个边是一组规则,它们定义哪些异常引起回滚,哪些不引起。在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,也可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚。
二、测试
数据库
entity
package syn;
public class transactiontestEntity {
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
dao
package syn;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class testDAO {
private JdbcTemplate jdbcTemplate = new JdbcTemplate();
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/*
插入行
*/
private static String add = "INSERT INTO transactiontest (name,age) VALUES(?,?)";
public void insertObject(String name, int age){
transactiontestEntity transactiontestEntity = new transactiontestEntity();
transactiontestEntity.setName(name);
transactiontestEntity.setAge(age);
Object[] args = {transactiontestEntity.getName(),transactiontestEntity.getAge()};
jdbcTemplate.update(add, args);
}
/*
通过名称删除行
*/
private static String del = "DELETE FROM transactiontest WHERE name=?";
public void deleteObject(String name){
Object[] args= {name};
jdbcTemplate.update(del, args);
}
/*
通过名称查找
*/
private static String find = "SELECT id,name,age FROM transactiontest WHERE name=?";
public List<transactiontestEntity> findbyName(String name){
Object[] args = {name};
return jdbcTemplate.query(find, args, new entityMapper());
}
/*
通过名称查找数量
*/
private static String findnum = "SELECT COUNT(*) FROM transactiontest WHERE name=?";
public int getMatchCount(String name){
Object[] args = {name};
return jdbcTemplate.queryForObject(findnum,args,Integer.class);
}
/*
删除所有行
*/
private static String delAll = "DELETE FROM transactiontest";
public void deledteAll(){
jdbcTemplate.update(delAll);
}
}
service
package syn;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
@Service
public class tmTest {
@Autowired
private testDAO testDAO;
public void setTestDAO(testDAO testDAO) {
this.testDAO = testDAO;
}
/*
写入数据
*/
@Transactional
public void setContent(String name){
int Age = (int)(Math.random()*100);
testDAO.insertObject(name,Age);
System.out.println("写入 "+name+" "+Age+" :"+new Date());
}
/*
按名称读取数据
*/
@Transactional
public void readLine(String name){
int num = testDAO.getMatchCount(name);
if(num>0) {
List<transactiontestEntity> result = testDAO.findbyName(name);
for (int i = 0; i < result.size(); i++) {
transactiontestEntity entity = result.get(i);
System.out.println("读取 " + entity.getName() + " " + entity.getAge() + " :" + new Date());
}
}
else
System.out.println("No such name now.");
}
/*
按名称删除数据
*/
@Transactional
public void deleteLine(String name){
int num = testDAO.getMatchCount(name);
if(num>0){
testDAO.deleteObject(name);
System.out.println("删除 "+name+" "+num+" 个 :"+new Date());
}
else
System.out.println("没有找到 "+name+ "删除失败");
}
}
entitymapper
package syn;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
public class entityMapper implements RowMapper<transactiontestEntity> {
@Override
public transactiontestEntity mapRow(ResultSet rs, int rowNum)throws SQLException{
transactiontestEntity transactiontestEntity = new transactiontestEntity();
transactiontestEntity.setId(rs.getInt(1));
transactiontestEntity.setName(rs.getString(2));
transactiontestEntity.setAge(rs.getInt(3));
return transactiontestEntity;
}
}
测试类
package syn;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import java.util.Random;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class tmTestTest {
// action ac = new action();
private String[] nameList = {"like", "littlelu", "Xiaoke", "like123", "keke", "LK"};
private Random random = new Random();
// private tmTest tmTest = new tmTest();
@Autowired
private tmTest tmTest;
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Test
@Transactional
public void TestThread(){
for(int i=0;i<100;i++){
threadPoolTaskExecutor.execute(new Runnable() {
@Override
public void run() {
tmTest.setContent(nameList[random.nextInt(5)]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadPoolTaskExecutor.execute(new Runnable() {
@Override
public void run() {
tmTest.readLine(nameList[random.nextInt(5)]);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadPoolTaskExecutor.execute(new Runnable() {
@Override
public void run() {
tmTest.deleteLine(nameList[random.nextInt(5)]);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
测试并发环境下对数据库增删查
运行效果: