Spring 并发事务的探究

  • 前言

    在目前的软件架构中,不仅存在单独的数据库操作(一条SQL以内,还存在逻辑性的一组操作。而互联网软件系统最少不了的就是对共享资源的操作。比如热闹的集市,抢购的人群对同见商品的抢购由一位售货员来处理,这样虽然能保证买卖的正确进行,但是牺牲了效率,饱和的销售过程并不能高效处理所有的购买请求,最后打烊了部分顾客悻悻而归。而电脑的发明是让人类解放于这种低效的工作中,提高销售性能,比如抢购系统,秒杀系统等。而这种销售过程必然包含了检查库存、秒杀排队、校对商品信息、下单等一系列的组合操作,而一个交易过程再怎么解耦,仍然无法做到单条数据操作达到最终数据一致性,因为在比如抢购和库存-1这种操作中,必然要使得其逻辑一致。

    我认为,世界上只有两种资源:一种是皇上享有的资源,一种是大众享有的资源。如果不能确定这个资源只有一个用户的话,那就必然涉及到竞争。而多元问题只需要研究二元模型就可以。比如相互独立事件P(X,Y) = P(X)*P(Y),进而两两独立的事件P(X,Y,Z) = P(X)*P(Y)*P(Z)一样,只需要研究两个用户会产生什么样的行为就可以对业务进行精确的设计了。而数据库有一种处理并发操作的设计:数据库事务。

    这次就来总结一下本人最近探究的数据库事务的并发模型以及模拟一些会发生的情况,由于缺少大并发的经验,只能立足于书本了。本次的环境是基于上篇搭建的maven项目以及使用Spring事务。

  • 基本知识

    Mysql数据库,Mysql事务,Spring事务管理。

    首先啰嗦一下,对于Maven项目的编译配置,上篇博客中漏掉了编译打包的时候带上properties文件,导致弄了一下午不知道为什么起不来,在此记录更正一下。主要是tomcat的报错太过隐秘,导致我看不到它的编译错误。

    为了方便,配置了虚拟映射路径,配置方法是打开tomcat/conf/server.xml,找到<Host>标签,添加Context。

        <Context path="/" docBase="/Users/MacBook/Documents/test1/target/test1" reloadable="true" debug="0" />

    docBase写项目路径,一般maven项目都会有个target。这样访问项目的时候就是http://localhost:8080/,就可以访问到你的目录了。每次启动tomcat都是一组sh,所以写了个脚本,顺便看看日志。

#!/bin/sh
killall -9 java
cd /Users/MacBook/Documents/test1
mvn package
sh /Users/MacBook/Documents/tomcat7/bin/startup.sh
tail -f /Users/MacBook/Documents/tomcat7/logs/catalina.out

    maven的pom.xml中要加入编译插件更正的部分,否则打包后丢失properties文件。

  <build>
    <finalName>test1</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>

    </plugins>

    <!-- 解决Maven项目编译后classes文件中没有.xml问题 -->
    <resources>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>**/*.xml</include>
          <include>**/*.properties</include>
        </includes>
        <filtering>true</filtering>
      </resource>
      <resource>
        <directory>src/main/java/resources</directory>
      </resource>
    </resources>
  </build>

    好,接下来进入正题了。数据库事务通常存在四种特性,概念在很早之前已经总结过了,ACID。而利用隔离性来控制并发事务并保证数据一致性,是根据情况来的。什么是根据情况呢?就是业务上,如果这个数据出现这种情况是合法的,那么尽量牺牲隔离性换取性能,如果数据是强一致的,那就牺牲性能换一致性。

    Spring的事务中存在几种隔离级别,都是世界公理了,只需要在@Transactional注释里配置一下就好了。

    @Transactional(rollbackFor=Exception.class,readOnly = false, propagation = Propagation.REQUIRED,isolation = Isolation.READ_UNCOMMITTED)

    进入源码中查看隔离级别的种类。它是个枚举类型,隔离性的英文和数据库的隔离是一样的。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.transaction.annotation;

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

     在执行事务之前,我先总结一下事务的必要条件。基于MySQL的事务,首先表的类型要是Innodb,有次实验一直不触发回滚,后来发现表的类型是Myisam。

    事务的原理基于数据库的begin,commit。在这两个命令之间的数据库操作,如果事务中夹杂着缓存操作,那是回滚不了的只能显示回滚了。还有Spring事务的异常回滚,是基于动态代理技术,如果不抛出异常,在Dao层把异常生吞了,后续也没抛出异常,那是回滚不了的了。异常必须是在@Transactional标注的那个函数层被识别,这样才有回滚的余地。

    现在进入本次研究的正题,并发事务。并发事务在不同的隔离级别下会产生的异常在资料中存在:脏读、幻读、不可重复读

    概念的表述如下

    1.脏读:一个事务读取到另一个事务未提交的数据。也就是begin之后update了一条事务,但是没有commit,另一个事务读取相同数据发现是它修改但是未提交的数据。

    2.幻读:一个事务在两次查询相同条件的时候,另一个事务执行插入事务,导致前一个事务在两次查询中返回了不同的结果,宛如产生了幻觉一般。

    3.不可重复读:一个事务读取到另一个事务更新后的数据。前一个事务两次查询,出现不一致的结果,后一个事务在两次查询中修改了这个数据的内容并提交。

    发生脏读的隔离级别是最低的,使用READ_UNCOMMITTED隔离级别就可以模拟出来了。

    首先编写同一个数据库接口。

package Dao;

import org.springframework.jdbc.core.JdbcTemplate;

import java.util.Map;

/**
 * Created by MacBook on 2017/11/18.
 */
public class TxAddressDao {
    private JdbcTemplate jdbcTemplate;
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    //写入
    public void insertCol(String address,String remark){
        String sql = "insert into address(address,remark) values(?,?)";
        Object[] args = {address,remark};
        try{
            jdbcTemplate.update(sql,args);
        }catch (Exception e){
            throw new RuntimeException();
        }
    }
    //更新
    public void updateCol(long id,String address,String remark){
        String sql = "update address set address = ?,remark = ? where id= ?";
        Object[] args = {address,remark,id};
        try{
            jdbcTemplate.update(sql,args);
        }catch (Exception e){
            throw new RuntimeException();
        }
    }
    //查找
    public Map<String,Object> selectCol(long id){
        String sql = "select address,remark from address where id = ?";
        try{
            return jdbcTemplate.queryForMap(sql,id);
        }catch (Exception e){
            throw new RuntimeException();
        }
    }
    //查询数量
    public int selectFromTo(long idFrom,long idTo){
        String sql = "select count(*) from address where id > ? and id < ?";
        Object[] args = {idFrom,idTo};
        try{
            return jdbcTemplate.queryForInt(sql,args);
        }catch (Exception e){
            throw new RuntimeException();
        }
    }

}
  • 脏读模拟

    编写一个脏读业务和一个更新业务。

    @Transactional(rollbackFor=Exception.class,readOnly = false, propagation = Propagation.REQUIRED,isolation = Isolation.READ_UNCOMMITTED)
    public Map<String,Object> dirtyRead(long id){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("脏读事务开始:"+sdf.format(new Date()));
        Map<String,Object> data = txAddressDao.selectCol(id);
        System.out.println("脏读事务结束:"+sdf.format(new Date()));
        return data;

    }
    @Transactional(rollbackFor=Exception.class,readOnly = false, propagation = Propagation.REQUIRED,isolation = Isolation.READ_UNCOMMITTED)
    public void updateData(long id,String address){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("更新事务开始:"+sdf.format(new Date()));
        Date d = new Date();
        String time = sdf.format(d);
        txAddressDao.updateCol(id,address,time);
        try{
            Thread.sleep(10000);//10秒
        }catch (Exception e){}
        System.out.println("更新事务j结束:"+sdf.format(new Date()));
    }

    这里在更新后睡眠十秒钟,在此过程内是不会提交事务的。使用postman模拟请求,轻松实现脏读现象。

    在这个接口还在pedding的时候,调用另外一个方法读取,发现已经读到了更新的数据了。

    在时间上,脏读事务处于更新事务区间内,模拟了一次脏读,如果把隔离级别提升,则这个现象将会消失。

    提升了隔离级别之后,再次模拟。

  • 不可重复读模拟

    在READ_COMMITTED中,会发生不可重复读,即两次select会产生不一样的结果。

    //不可重复读模拟
    @Transactional(rollbackFor=Exception.class,readOnly = false, propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
    public Map<String,Object> unrepeatableRead(long id){
        Map<String,Object> data = txAddressDao.selectCol(id);
        System.out.println("第一次读取");
        for(String key:data.keySet()){
            System.out.println("key :"+key+" value :"+data.get(key));
        }
        try{
            Thread.sleep(5000);//5秒
        }catch (Exception e){}
        data = txAddressDao.selectCol(id);
        System.out.println("第二次读取");
        for(String key:data.keySet()){
            System.out.println("key :"+key+" value :"+data.get(key));
        }
        return data;

    }

    命令行模拟打印出了不可重复读的模拟结果。在一次事务中读到了不同结果,在实际业务中会产生数据不一致的问题。

  • 幻读模拟  

    幻读会发生在REPEATABLE_READ以下的隔离级别,首先新建一个事务,检查这个id是否有插入过,然后插入这条数据,在这个事务执行过程中另一个事务插入了这个id为主键的数据,最终导致第一个事务失败,这个id不存在的查询宛如幻觉一般。

    //幻读模拟
    @Transactional(rollbackFor=Exception.class,readOnly = false, propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ)
    public void phantomRead(long id,String address){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        int col = txAddressDao.idExist(id);
        System.out.println("第一次读取区间内数据:"+col);
        try{
            Thread.sleep(5000);//10秒
        }catch (Exception e){}
        if(col == 0){
            Date d = new Date();
            String time = sdf.format(d);
            txAddressDao.insertCol(id,address,time);
        }
    }

  • 结语

    以往只是探索了概念,本次亲自做了一下模拟,对性能有了更深刻的感知,要应用带工作中的技术不能一知半解,必须知道如何控制它,让它朝着你预想的方向走。比如我保证事务中的串行执行,只要有一个环节出现超乎预想就要回滚,如何回滚。或者某些异常回滚,某些不回滚。还有哪种隔离级别适合哪种场景。以及初步了解数据库事务对哪方面的操作是可以回滚的、Spring事务是运用了什么思路设计的。

    最后,倒腾了一天主要耗时不在事务研究,而在服务器配置maven工程配置之类的。

    

时间: 2024-08-01 11:09:36

Spring 并发事务的探究的相关文章

spring的事务

Chapter 1. Spring中的事务控制(Transacion Management with Spring) Table of Contents 1.1. 有关事务(Transaction)的楔子 1.1.1. 认识事务本身 1.1.2. 初识事务家族成员 1.2. 群雄逐鹿下的Java事务管理 1.2.1. Java平台的局部事务支持 1.2.2. Java平台的分布式事务支持 1.2.2.1. 基于JTA的分布式事务管理 1.2.2.1.1. JTA编程事务管理 1.2.2.1.2.

Spring:事务

摘要 本文摘抄了Spring事务相关的一些理论,主要讲述事务的特性.事务的传播行为.事务的隔离规则. 关键词:事务特性,事务传播,事务隔离 一.什么是事务 事务是用来保证数据的完整性和一致性,正如金钱转账,金钱总数不会增加也不会减少. 数据库 事务管理有四个特性(ACID): 特性 描述 原子性(Atomicity) 事务作为一个整体被执行,要么全部被执行,要么都不执行. 一致性(Consistency) 事务应确保数据的状态从一个一致状态转变为另一个一致状态,数据不应该被破坏. 隔离性(Iso

spring与事务管理

就我接触到的事务,使用最多的事务管理器是JDBC事务管理器.现在就记录下在spring中是如何使用JDBC事务管理器 1)在spring中配置事务管理器 <!-- JDBC事务 -->    <bean id="jdbcTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">        <property

Spring中事务管理

1.什么是事务? 事务是逻辑上的一组操作,这组操作要么全部成功,要么全部失败 2.事务具有四大特性ACID 1)原子性(Atomicity):即不可分割性,事务要么全部被执行,要么就全部不被执行.如果事务的所有子事务全部提交成功,则所有的数据库操作被提交,数据库状态发生转换:如果有子事务失败,则其他子事务的数据库操作被回滚,即数据库回到事务执行前的状态,不会发生状态转换. 2)一致性(Consistency):事务的执行使得数据库从一种正确状态转换成另一种正确状态.例如对于银行转账事务,不管事务

spring的事务控制

1.事务介绍 (1)特性:ACID Atomicity(原子性):事务中的所有操作要么全做要么全不做 Consistency(一致性):事务执行的结果使得数据库从一个一致性状态转移到另一个一致性状态 Isolation(隔离性):一个事务的执行不受其他事务的干扰 Durability(永久性):一个事务一旦提交,对数据库的影响是永久性的 (2)事务并发问题 (3)       隔离级别 2.  Spring封装了事务管理操作 1.事务操作 打开事务  回滚事务   提交事务 2.事务操作对象 因

Spring的事务属性

1.事务Transactional下的属性 @Transactional( readOnly = false, // 读写事务,只读事务 timeout = -1, // 事务的超时时间不限制 //noRollbackFor = ArithmeticException.class, // 不回滚 = 条件.class isolation = Isolation.DEFAULT, // 事务的隔离级别,数据库的默认 propagation = Propagation.REQUIRED // 事务的

spring的事务操作

我们项目一期已经差不多结束了,所以一些细节也被拿了出来,出现最多的就是事务的操作了.因为自己负责的是一个模块(因为是另外一个项目的负责人),所以组员经常会遇到事务的问题,会出现很多奇葩的用法,各种乱用,估计他们就知道在方法上面注解@Transactional,但是其中的很多细节都不知道.所以经常会出现一个情况,就是一大坨代码出现了事务的问题,然后我就去各种改.所以今天也对事务做一个总结吧.以后忘记了可以回来看看. 一般我们使用事务最主要注重的是三个方面: 1.propagation:传播性  

spring,mybatis事务管理配置与@Transactional注解使用

spring,mybatis事务管理配置与@Transactional注解使用[转] 概述事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性.Spring Framework对事务管理提供了一致的抽象,其特点如下: 为不同的事务API提供一致的编程模型,比如JTA(Java Transaction API), JDBC, Hibernate, JPA(Java Persistence API和JDO(Java Data Objects) 支持声明式事务管理,特别是基

[Java]Spring数据库事务基础知识

Spring虽然提供了灵活方便的事务管理功能,但这些功能都是基于底层数据库本身的事务处理机制工作的.要深入了解Spring的事务管理和配置,有必要先对数据库事务的基础知识进行学习. 何为数据库事务 "一荣俱荣,一损俱损"这句话很能体现事务的思想,很多复杂的事物要分步进行,但它们组成一个整体,要么整体生效,要么整体失效.这种思想反映到数据库上,就是多个SQL语句,要么所有执行成功,要么所有执行失败. 数据库事务有严格的定义,它必须同时满足 4 个特性:原子性(Atomic).一致性(Co