事务的概念
事务通常用于数据库领域,指对数据库进行读或者写的一组操作序列,要么都执行,要么都不执行,不允许只执行一部分的情况;事务的操作结果只有两种,一种是操作成功,一种是操作不成功恢复到操作之前的状态。
事务的特性
- 原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。 - 一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。 - 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。 - 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
事务并发带来的问题:
- 脏读
事务A读取了事务B更新未提交的数据,接着事务B进行了回滚操作,所以事务A读取的数据是脏数据,称之为 脏读。 - 不可重复读
事务A多次读取同一个数据,事务B在事务A多次读取的过程中,对数据进行了修改并提交的操作,导致事务A多次读取的数据,结果不一致,称之为不可重复读。 - 幻读
事务A对数据库添加或者删除了数据,但是事务B中读取不到,也就是事务在开始读取数据的时候就像做了标记一样,只能读取到此刻的数据,后面数据库发生的变化,这个事务无法感知,这种情况,称之为幻读,读取到的数据与真实数据库的不一致,仿佛产生了幻像。
不可重复读和幻读的概念很容易混淆,不可重复读侧重于修改,而幻读侧重与添加和删除。解决不可重复读的问题,只需要锁住满足条件的行,解决幻读需要锁住整张表。
事务的隔离级别:
- 读未提交READ_UNCOMMITTED
- 读已提交READ_COMMITED
- 可重复读REPEATABLE_READ
- 串行化
MySql数据库默认采用的是可重复读,Oracle数据库默认采用的是读已提交。
各隔离级别与问题的关系:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 会 | 会 | 会 |
读已提交 | 不会 | 会 | 会 |
重复读 | 不会 | 不会 | 会 |
串行化 | 不会 | 不会 | 不会 |
事务的传播属性
- REQUIRED 支持当前事务,当前没有事务的话,创建一个新的事务
- REQUIRES_NEW 创建新的事务,当前存在事务的话,挂起当前食物
- NESTED 嵌套事务,支持当前事务,新增Savepoint点,与当前事务同步提交或回滚
- SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行
- NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
- MANDATORY 支持当前事务,如果当前没有事务,就抛出异常
- NEVER 以非事务方式执行,如果当前存在事务,则抛出异常
以上的传播属性中,最常用的,应该属于REQUIRED 和 REQUIRES_NEW
事务属性,除了隔离属性和传播属性之外,还有只读属性、超时属性以及异常属性,这些事务属性中,spring重点实现的是传播属性,其他的属性都是依托数据库或者jdbc驱动来实现的。
事务的思考
- 场景
有这样一个场景:我们在service层中总共会做三步的数据库持久化处理,但是,需要我们希望第一步处理之后,立即提交,将数据的更新锁定,防止误处理等,后续的两步处理,无论成功还是失败回滚,都不要将第一步的处理给回滚掉,那么我们在spring中如何来处理呢? - 思考
我们是否可以设置传播属性为NESTED,来实现上述场景的需求呢?或者设置为REQUIRES_NEW来实现?或者使用savepoint保存点的方式来实现? - 解决方案
- 方案一
NESTED不适用于此种场景,但是我们可以使用REQUIRES_NEW 来实现,将第一步操作封装在一个单独的方法,方法的事务属性设置为REQUIRES_NEW,让其处理不受另外两步的影响,但是,这种方案中,我们不能在service层中一个方法直接调用另外一个方法,否则被调用的方法上的事务会失效,你知道是什么原因么?
具体示例如下:- 代码:
- 方案一
//配置类
@Configuration
@ComponentScan("com.jdbc")
@EnableTransactionManagement
public class AppConfig {
@Bean
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql:///test?useSSL=false&allowPublicKeyRetrieval=true");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate template = new JdbcTemplate();
template.setDataSource(dataSource);
return template ;
}
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
return transactionManager ;
}
}
//service层
@Service
public class UserServiceImpl implements ApplicationContextAware {
private ApplicationContext context ;
@Autowired
private UserDao userDao ;
@Transactional(propagation = Propagation.REQUIRED)
public void register() {
UserServiceImpl service = (UserServiceImpl) context.getBean("userServiceImpl");
service.register2();
User user = new User();
user.setName("bbbbb");
user.setVers(1);
userDao.save(user);
int i = 10 / 0 ;//抛出异常,进行回滚
user = new User();
user.setName("ccccc");
user.setVers(1);
userDao.save(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void register2(){
User user = new User();
user.setName("aaaaa");
user.setVers(1);
userDao.save(user);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
}
//DAO层
@Repository
public class UserDaoImpl implements UserDao{
@Autowired
private JdbcTemplate template ;
@Override
public void save(User user) {
template.update("insert into t_user(name, vers) values (?, ?)", user.getName(), user.getVers());
}
@Override
public List<User> getAllUsers() {
List<User> users = template.query("select * from t_user", new BeanPropertyRowMapper<>(User.class));
return users;
}
}
/**
测试代码
**/
public class JdbcTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
UserServiceImpl service = (UserServiceImpl) ctx.getBean("userServiceImpl");
service.register();
}
}
测试结果:
可以看到,上述的测试结果符合我们的预期,REQUIRE_NEW这种传播属性的使用,满足了我们的需求
- 方案二: 使用savepoint的方式来实现,保存点属于数据库的高级属性,必须数据库以及jdbc的驱动都支持才可以使用
@Transactional(propagation = Propagation.REQUIRED)
public void register() {
User user = new User();
user.setName("aaaaa");
user.setVers(1);
userDao.save(user);
//此处添加savepoint,来进行保存点的设置,使回滚时,只回滚到此处
DefaultTransactionStatus status = (DefaultTransactionStatus) TransactionAspectSupport.currentTransactionStatus();
status.createAndHoldSavepoint();
user = new User();
user.setName("bbbbb");
user.setVers(1);
userDao.save(user);
int i = 10 / 0 ;//抛出异常,进行回滚
user = new User();
user.setName("ccccc");
user.setVers(1);
userDao.save(user);
}
测试结果
可以看到, bbbbb和ccccc都没有保存进数据库,但是aaaaa保存进了数据库,说明savepoint发挥了作用,我们在来看看日志中的信息: