002-JDBC控制事务

JDBC控制事务

(一) 事务基本认识

(1) 事务概述

事务指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)

事务就是用来管理一个包含多个步骤的业务操作,这些操作要么同时成功,要么同时失败!

而事务需要满足4个属性,即ACID属性:

原子性(atomicity)一致性(consistency)隔离性 (isolation)和持久性(durability)

  • 原子性:一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做

  • 一致性:事务操作前后,数据的总量不发生变化

  • 隔离性:一个事务的执行不能被其他事务干扰,相互独立

  • 持久性:一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的

(2) 为什么使用事务

举一个简单的例子:A账户要转给B账户500元,A账户的余额首先要减去500元,然后再在B账户的余额中增加500,但是如果在转账的过程中,由于网络原因或者程序内部异常问题而导致操作失败,那么注定我们的业务是失败的,但是我们必须做出一些控制措施保证业务的正确性,这时候就需要使用事务,即,A账户金额的减少和B账户金额的增长,写到一个事务中去,要么同时成功,要么同时失败!

写一个小Demo看一下,先创建一张简单的表,账户A:admin 账户B:zhangsan 初始余额均为1000

CREATE TABLE account(
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(32),
    balance VARCHAR(32)

);

INSERT INTO account VALUES(NULL,'admin','1000');
INSERT INTO account VALUES(NULL,'zhangsan','1000');

Java代码

package cn.ideal.transaction;

import cn.ideal.jdbc.JDBCUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 * 本例中使用了自定义的JDBCUtils类,详情可以参考以前的文章,或者按照默认方法连接数据库等
 */
public class TransactionDemo {
    public static void main(String[] args) {

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = JDBCUtils.getConnection();
            String sql = "UPDATE account SET balance = balance - 500 WHERE id = 1";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.executeUpdate();

            String sql2 = "UPDATE account SET balance = balance + 500 WHERE id = 2";
            preparedStatement = connection.prepareStatement(sql2);
            preparedStatement.executeUpdate();

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(preparedStatement, connection);
        }
    }
}

//数据库中结果
admin 余额:500
zhangsan 余额:1500

我们模拟中途发生错误的情况

connection = JDBCUtils.getConnection();
String sql = "UPDATE account SET balance = balance - 500 WHERE id = 1";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.executeUpdate();

//模拟错误发生在这里
int a = 100/0;

String sql2 = "UPDATE account SET balance = balance + 500 WHERE id = 2";
preparedStatement = connection.prepareStatement(sql2);
preparedStatement.executeUpdate();

//运行结果
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at cn.ideal.transaction.TransactionDemo.main(TransactionDemo.java:22)
//数据库中的结果
admin 余额:500
zhangsan 余额:1000

这个时候事务的重要性就体现出来了

//代码节选
try {
    //开启会务,对数据的操作就不会立即生效
    connection.setAutoCommit(false);

    connection = JDBCUtils.getConnection();
    String sql = "UPDATE account SET balance = balance - 500 WHERE id = 1";
    preparedStatement = connection.prepareStatement(sql);
    preparedStatement.executeUpdate();

    //模拟错误
    int a = 100 / 0;

    String sql2 = "UPDATE account SET balance = balance + 500 WHERE id = 2";
    preparedStatement = connection.prepareStatement(sql2);
    preparedStatement.executeUpdate();

    //如果程序到此仍无异常,则提交数据
    connection.commit();

    //关闭事务
    connection.setAutoCommit(true);

} catch (SQLException e) {
    try {
        //如果出现异常,则事务回滚,即数据恢复原来的样子
        connection.rollback();
        //关闭事务
        connection.setAutoCommit(true);
    } catch (SQLException e1) {
        e1.printStackTrace();
    }
} finally {
    JDBCUtils.close(preparedStatement, connection);
}

现在在中途出错的情况下,上面的程序也依旧抛出了异常,但是A账户余额没有减少,B账户余额也没有增多,我们所需要的效果也就达到了

(二) savepoint

savepoint 即设置保留点,事务可以回到 savepoint 而不影响 savepoint 前的变化,即不需要放弃整个事务,回滚到想要的位置

//声明一个保留点
SAVEPOINT 保留点名
//回滚到保留点
ROLLBACK TO 保留点名
//删除指定保留点(MySQL5及以后)
RELEASE SAVEPOINT 保留点名

(三) 事务的隔离级别

事务是满足隔离性的,即一个事务的执行不能被其他事务干扰,相互独立,但是如果多个事务去操作同一批数据,一些问题则会出现,而数据库定义了4个隔离界别就解决了这些问题

(1) 隔离级别

补充概念:

脏读

脏读又称无效数据的读出,是指在数据库访问中,事务T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2所读取到的数据是无效的。

也就是说 :脏读,即读取到不正确数据,另一个事务可能还没提交数据,但这个事务已经读取了中间数,这个数据不能保证正确

不可重复读

指在数据库访问中,一个事物范围内两个相同的查询却返回了不同数据,较为容易理解的说法:在一个事务内读取两次同一个数据,在两次读取中途,另一个事务也访问同一数据并且修改,那么第一个事务两次读取的结果就有可能不同

幻读

幻读是指当事务不是独立执行时发生的一种现象。

幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。

A:Read uncommitted:读未提交

存在的问题脏读不可重复读幻读

假设场景:期末阅卷结束后,老师在教务系统录入分数,99分一不小心就点成了59分,但在正式系统未公布,学生通过另一台内部的系统已经查到自己分数为59分,后来老师及时发现了录入错误,将尚未提交的事物滚回,将分数修改正确

B:Read committed:读已提交 (Oracle)

存在的问题不可重复读幻读

假设场景:两个人共用一张银行卡,卡中有2000元,我看中一个1500元的键盘,准备刷卡,刷卡机检测到我还有2000余额,但是这时候,我女朋友买化妆品花掉了1800元,并且提交,而我刷卡机准备正式从我卡中扣钱的时候发现已经余额不足

C:Repetable read:可重复读 (MySQL默认)

存在的问题幻读

假设场景:当刷卡机一旦读取到我的余额后,其他用户就不能再进行操作了,直到我的操作结束

D:Serializable:序列化

可以解决一切问题

是事务隔离级别最高的,但是这种事务隔离级别效率低下,比较耗数据库性能,一般不是很推荐使用

简单归纳:脏读绝对不能存在,不可重复读和幻读在一定情况下可以存在

结尾:

如果内容中有什么不足,或者错误的地方,欢迎大家给我留言提出意见, 蟹蟹大家 !^_^

如果能帮到你的话,那就来关注我吧!(系列文章均会在公众号第一时间更新)

在这里的我们素不相识,却都在为了自己的梦而努力 ❤

一个坚持推送原创Java技术的公众号:理想二旬不止


   转载规则


《002-JDBC控制事务》 BWH_Steven 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录