欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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");
}

相关链接:我的Professional Java for Web Applications相关文章