Java for Web学习笔记(一三七)篇外之数据库的ACID和JPA(1)原子性
程序员文章站
2022-04-22 13:40:34
...
ACID大家都听过,看似也了解,但是在实际的项目中,发现不是所有人都正确理解。所以想谈一下当中容易忽略或者错误理解的地方。现在的开发语言和开发工具都很丰富,如果这要一一了解,也真是耗不起,但是有些是工具,知道怎么用就行,有些是基础知识,需要掌握。ACID就是基础知识,只有真正了解,才能在代码中进行合适的选择,特别是在异常处理和并发处理。
在这个小系列中,基于下面的表格
CREATE TABLE `test_acid`(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`score` int(5) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SQL的事务
通过SQL语句演示一个事务的回滚
DELIMITER $$
CREATE PROCEDURE `mytest_acid_rb`()
BEGIN
DECLARE `_rollback` INTEGER DEFAULT 0;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET `_rollback` = 1;
START TRANSACTION;
SELECT @A:= score FROM test_acid WHERE id=1;
UPDATE test_acid SET [email protected]+1 WHERE id=1;
UPDATE test_acid SET [email protected] WHERE id=1; -- 由于score是非负整数,这里可能会抛出异常
IF `_rollback` THEN
ROLLBACK;
ELSE
COMMIT;
END IF;
SELECT * FROM test_acid WHERE id=1; -- 查看结果
END$$
DELIMITER ;
使用Java代码
下面Java代码采用Spring Data,基于Hibernate。
(1)SQL语句出错的情况
@Transactional
public void acidTest(){
TestAcidEntity entity1 = testAcidRepository.findOne(1L);
TestAcidEntity entity2 = testAcidRepository.findOne(2L);
entity1.setScore(entity1.getScore() + 1);
testAcidRepository.save(entity1);
// 出错,抛出一个org.hibernate.exception.DataException的异常,其继承了RuntimeException
entity2.setScore(-1);
testAcidRepository.save(entity2);
}
我们跟踪执行的SQL,可以看mysql的log,也可以用抓包工具(将jdbc url中参数设置:useSSL=false)
SELECT @@session.tx_isolation -- response:READ-COMMITTED
-- SET autocommit=0可以视为是transaction的开始。没有跟踪到START TRANSACTION
SET autocommit=0
select testaciden0_.id as id1_13_0_, testaciden0_.score as score2_13_0_ from test_acid testaciden0_ where testaciden0_.id=1
select testaciden0_.id as id1_13_0_, testaciden0_.score as score2_13_0_ from test_acid testaciden0_ where testaciden0_.id=2
update test_acid set score=6 where id=1
-- 下一句将response:Error 1264 Out of range value for column 'score' at row 1
update test_acid set score=-1 where id=2
rollback
SET autocommit=1
(2)非SQL语句的其他Java代码出错
对于Exception,分为checked exception,这类是需要在代码中明确进行异常捕获或抛出给调用者,在编译的时候进行检查;另一类是 RuntimeException ,在运行是出现,不需要在代码中显示进行异常捕获或者抛出给调用者。对于JPA的transaction而言,出现了RuntimeException会触发rollback;而对于需要抛出的异常Checked Exception,将在结束方法抛出异常之前进行commit,也就是说之前的写操作会发送出去并且执行。
提供一个测试小例子:
@Transactional
public void acidTest() throws Exception{
TestAcidEntity entity1 = testAcidRepository.findOne(1L);
TestAcidEntity entity2 = testAcidRepository.findOne(2L);
entity1.setScore(6);
testAcidRepository.save(entity1);
// 【另一个小测试】再读一次,并将结果打印出来。
// 会发现实际上并没有真的发送SQL,当id相同的时候,JPA只读取一次;
// 如果代码进行了修改(虽然没有实际发送),则为修改后的值。这是JPA内部的实现。
TestAcidEntity entityCheck = testAcidRepository.findOne(1L);
log.info("score1 = {}",entityCheck.getScore()); //发现score为6
doSomething(); //抛出Exception,这是checked exception,在方法声明中抛出
// 抛出异常后面的代码就不执行了,那么在方法结束之前,将之前相关的写操作执行。
// update test_acid set score=6 where id=1(签名的save)
// commit
entity2.setScore(2);
testAcidRepository.save(entity2);
}
private void doSomething() throws Exception{
throw new Exception("Just for test");
}
上面的小例子并没有出现回滚,执行了第一个update,不执行第二个update。在抛出异常时commit,后面的代码不执行。
我们再看看RunnableException的情况,这是会出发回滚的。
@Transactional
public void acidTest(){
...... 同上,只是不在方法声明中抛出异常
}
private void doSomething() {
throw new RunnableException("Just for test");
}