资讯专栏INFORMATION COLUMN

聊聊JDBC事务隔离级别(修正)

phpmatt / 1288人阅读

摘要:在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。隔离性一个事务的执行不能被其他事务所影响。比如,事务在事务提交前读到的结果,和提交后读到的结果可能不同。

重要

由于之前代码的不严谨,导致结果和结论的错误,深表歉意,现在对其进行修正

摘要

事务在日常开发中是不可避免碰到的问题,JDBC中的事务隔离级别到底会如何影响事务的并发,脏读(dirty reads), 不可重复读(non-repeatable reads),幻读(phantom reads)到底是什么概念

事务

原子性(atomicity) 事务是数据库的逻辑工作单位,而且是必须是原子工作单位,对于其数据修改,要么全部执行,要么全部不执行。

一致性(consistency) 事务在完成时,必须是所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。

隔离性(isolation) 一个事务的执行不能被其他事务所影响。

持久性(durability) 一个事务一旦提交,事物的操作便永久性的保存在数据库中,即使此时再执行回滚操作也不能撤消所做的更改。

隔离性

以上是数据库事务-ACID原则,在JDBC的事务编程中已经为了我们解决了原子性,持久性的问题,唯一可配置的选项是事务隔离级别,根据com.mysql.jdbc.Connection的定义有5个级别:

TRANSACTION_NONE(不支持事务)

TRANSACTION_READ_UNCOMMITTED

TRANSACTION_READ_COMMITTED

TRANSACTION_REPEATABLE_READ

TRANSACTION_SERIALIZABLE

读不提交(TRANSACTION_READ_UNCOMMITTED)

不能避免dirty reads,non-repeatable reads,phantom reads

读提交(TRANSACTION_READ_COMMITTED)

可以避免dirty reads,但是不能避免non-repeatable reads,phantom reads

重复读(TRANSACTION_REPEATABLE_READ)

可以避免dirty reads,non-repeatable reads,但不能避免phantom reads

序列化(TRANSACTION_SERIALIZABLE)

可以避免dirty reads,non-repeatable reads,phantom reads

创建一个简单的表来测试一下隔离性对事务的影响

CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
脏读(dirty reads)

事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的就是脏数据。

TEST:
事务A: update account += 1000, 然后回滚
事务B: 尝试读取 account 的值
期望结果:
当设置隔离级别为TRANSACTION_READ_UNCOMMITTED时,事务B读取到的值不一致
当设置隔离级别大于TRANSACTION_READ_UNCOMMITTED时,事务B读取到的值一致

先创建一个read任务

class ReadTask implements Runnable {
    int level = 0;
    
    public ReadTask(int level) {
        super();
        this.level = level;
    }

    @Override
    public void run() {
        Db.tx(level, new IAtom() {
            @Override
            public boolean run() throws SQLException {
                AccountService service = new AccountService();
                System.out.println(Thread.currentThread().getId() + ":" + service.audit());
                return true;
            }
        });
    }
}

其中AccountService代码(提供了读和写balance的方法)

public class AccountService {
    
    // 貌似这个方法有执行了行锁
    public void deposit(int num) throws Exception {
        int index = Db.update("update account set balance = balance + " + num + " where user_id = 1");
        if(index != 1)
            throw new Exception("Oop! deposit fail.");
    }
    
    public int audit() {
        return Db.findFirst("select balance from account where user_id = 1").getInt("balance");
    }
}

PS: 上述代码所使用的框架为JFinal(非常优秀的国产开源框架)

对于Db.findFirst和Db.update这2个方法就是对JDBC操作的一个简单的封装

然后再创建一个writer任务

class WriterTask implements Runnable {
    int level = 0;
    
    public WriterTask(int level) {
        super();
        this.level = level;
    }
    
    @Override
    public void run() {
        Db.tx(level, new IAtom() {
            @Override
            public boolean run() throws SQLException {
                AccountService service = new AccountService();
                try {
                    service.deposit(1000);
                    System.out.println("Writer 1000.");
                    Thread.sleep(1000);
                    System.out.println("Writer complete.");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return false;
            }
        });
    }
}

然后执行主线程

public static void main(String[] args) throws Exception {
    int level = Connection.TRANSACTION_READ_UNCOMMITTED;
    for(int j = 0; j < 10; j++) {
        if(j == 3) new Thread(new WriterTask(level)).start();
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(new ReadTask(level)).start();
    }    
}

上诉代码开启ReadTask和WriterTask对balance的值进行并发的写入和读取,并且WriterTask最终会回滚事务

当隔离级别为TRANSACTION_READ_UNCOMMITTED时,发现在WriterTask-commit事务前后读取到的值不一样

13:0
14:0
15:0
Writer 1000.
17:1000
18:1000
19:1000
Writer complete.
20:0
21:0
22:0
23:0

然后修改代码的隔离级别为TRANSACTION_READ_COMMITTED,发现前后读取的值一致,但是值得注意的是的,数据一致是建立在WriterTask事务回滚的情况下,如果事务正确的提交了,还是有出现数据不一致的问题,关于数据的一致性就不能简单的使用事务隔离来解决了,需要lock,关于数据一致的问题不在本文章讨论内

13:0
14:0
15:0
Writer 1000.
17:0
18:0
19:0
Writer complete.
20:0
21:0
22:0
23:0
不可重复读(non-repeatable reads)

在同一个事务中,对于同一份数据读取到的结果不一致。比如,事务B在事务A提交前读到的结果,和提交后读到的结果可能不同。

TEST:
事务A: update account += 1000, 然后commit
事务B: 尝试读取 account 的值(间隔2秒),再次尝试读取

为了满足不可重复读的测试对ReadTask作一些小改动

class ReadTask2 implements Runnable {
    int level = 0;
    
    public ReadTask2(int level) {
        super();
        this.level = level;
    }

    @Override
    public void run() {
        Db.tx(level, new IAtom() {
            @Override
            public boolean run() throws SQLException {
                AccountService service = new AccountService();
                System.out.println(Thread.currentThread().getId() + ":" + service.audit());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getId() + ":" + service.audit());
                return true;
            }
        });
    }
}

在代码中间隔2s,然后重复访问同一个balance字段

主线程代码

public static void main(String[] args) throws Exception {
    int level = Connection.TRANSACTION_REPEATABLE_READ;
    new Thread(new ReadTask2(level)).start();
    Thread.sleep(1500);
    new Thread(new WriterTask2(level)).start();
    Thread.sleep(1500);
}

设置隔离界别为TRANSACTION_READ_UNCOMMITTED

10:17000
Writer 1000.
10:18000

设置隔离界别为TRANSACTION_REPEATABLE_READ

10:18000
Writer 1000.
10:18000

读取到的1800是WriterTask事务未提交之前的值,假如要实时的获取balance的最新值,WriterTask很显然还是需要加lock,所以无可重复读的隔离级别只是避免了在同一个事务中数据读取的一致性,而不保证最终的数据一致性

幻读(phantom reads)

在同一个事务中,同一个查询多次返回的结果不一致。

ReadTask和WriterTask分别进行insert的sql与select的操作(select count(*) from account)

TEST:
事务A: insert account 然后commit
事务B: 尝试读取 account 的数量(间隔2秒),再次尝试读取

设置隔离界别为TRANSACTION_READ_COMMITTED

12:0
create account.
12:1

设置隔离界别为TRANSACTION_REPEATABLE_READ

12:1
create account.
12:1

设置隔离界别为TRANSACTION_SERIALIZABLE

12:2
create account.
12:2

关于最高级别序列化是只有当一个事务完成后才会执行下一个事务,但是这里我测试使用TRANSACTION_REPEATABLE_READ级别是还是避免了幻读,不知道是程序的问题还是JDBC的问题,这里我可能还需要进一步的测试和研究,但是根据官方对TRANSACTION_REPEATABLE_READ的说明

A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented. This level includes the prohibitions in TRANSACTION_REPEATABLE_READ and further prohibits the situation where one transaction reads all rows that satisfy a WHERE condition, a second transaction inserts a row that satisfies that WHERE condition, and the first transaction rereads for the same condition, retrieving the additional "phantom" row in the second read.

表示幻读的定义是在同一个事务中,读取2次的值是不一样的,因为有其他事务添加了一行,并且这行数据是满足第一个事务的where查询条件的数据

总结

本次测试使用JFinal框架(它对JDBC进行了很简易的封装),使用不同的隔离级别对3种并发情况进行测试,但是在幻读的测试中TRANSACTION_REPEATABLE_READ级别同样也避免了幻读的情况,这个有待进一步测试和研究

补充说明

同一个事务: 在JDBC编程中同一个事务意味着拥有相同的Connection,也就是说如果想保证事务的原子性所有的执行必须使用同一个Connection,事务的代表就是Connection

commit和rollback:在JDBC编程中一旦代码commit成功就无法rollback,所以一般rollback是发生在commit出现异常的情况下

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65096.html

相关文章

  • Java知识点总结(JDBC-事务

    摘要:隔离级别个等级的事务隔离级别,在相同的数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可以导致不同的结果。不同事务隔离级别能够解决的数据并发问题的能力是不同的。 Java知识点总结(JDBC-事务) @(Java知识点总结)[Java, JDBC] 事务 事务基本概念 一组要么同时执行成功,要么同时执行失败的 SQL 语句。是数据库操作的一个执行单元! 事务开始于:...

    Zachary 评论0 收藏0
  • JavaWEB开发13——事务与连接池

    摘要:一致性一个事务中,事务前后数据的完整性必须保持一致。持久性持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。 一、事务概述1.什么是事务一件事情有n个组成单元 要不这n个组成单元同时成功 要不n个单元就同时失败就是将n个组成单元放到一个事务中2.mysql的事务默认的事务:一条sql语句就是一个事务 默认就开启事务并提交事...

    13651657101 评论0 收藏0
  • Spring事务整理

    摘要:使用需要使用作为事务管理器。两个事务互不影响。这是默认的隔离级别,使用数据库默认的事务隔离级别下边的四个与的隔离级别相对应这是事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种事务隔离级别可 Spring事务整理 工作了几年了,今天抽时间整理一下spring的事务,说起spring的事务是面试的时候面试官经常提及的问题,接下来结合网上资料再总结下spring的事务...

    stackvoid 评论0 收藏0
  • JDBC常见面试题

    摘要:常见面试题操作数据库的步骤操作数据库的步骤注册数据库驱动。可以防止注入,安全性高于。只有隔离级别才能防止产生幻读。对象维护了一个游标,指向当前的数据行。一共有三种对象。 以下我是归纳的JDBC知识点图: showImg(https://segmentfault.com/img/remote/1460000013312769); 图上的知识点都可以在我其他的文章内找到相应内容。 JDBC...

    Yuqi 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<