Spring事務管理需要注意的點

大多數的人剛接觸到數據庫事務的時候肯定是一張懵圈臉,一臉迷茫透露着求知慾。在這給大家科普一些事務知識。:

  • 事務的概念
  • 事務的特徵
  • Mysql數據庫存儲引擎支持
  • 修改Mysql數據庫存儲引擎
  • spring配置事務
  • 測試代碼
  • 注意

事務的概念

事務(Transaction)是併發控制的單位,是用戶定義的一個操作序列。這些操作要麼都做,要麼都不做,是一個不可分割的工作單位。通過事務將邏輯相關的一組操作綁定在一起,以便服務器保持數據的完整性。
COMMIT:表示提交,即提交事務的所有操作。具體地說就是將事務中所有對數據庫的更新寫回到磁盤上的物理數據庫中去,事務正常結束。
ROLLBACK表示回滾,即在事務運行的過程中發生了某種故障,事務不能繼續進行,系統將事務中對數據庫的所有以完成的操作全部撤消,滾回到事務開始的狀態。

ex:A用網銀給B實現網上轉賬<這是一個場景>
第一種情況:A用戶金額減少B用戶金額增加<正常情況>
第二種情況:A用戶金額減少而在途中出現異常B用戶金額沒有增加<異常情況>
第三種情況:A用戶金額沒有減少而B用戶金額卻增加了<異常情況>
。。。
除去第一種情況,其餘的都是異常情況;這種情況下,就得使用事務。要麼做要麼不做即:A賬戶減少那麼B賬戶一定增加,否則A賬戶就不會減少。

事務的特徵

事務四大特性(簡稱ACID):原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)

原子性(Atomicity):事務中的全部操作在數據庫中是不可分割的,要麼全部完成,要麼均不執行。

一致性(Consistency):幾個並行執行的事務,其執行結果必須與按某一順序串行執行的結果相一致。

隔離性(Isolation):事務的執行不受其他事務的干擾,事務執行的中間結果對其他事務必須是透明的。

持久性(Durability):對於任意已提交事務,系統必須保證該事務對數據庫的改變不被丟失,即使數據庫出現故障。

Mysql數據庫存儲引擎支持

Mysql的存儲引擎:MyIsAm、InnoDB、MEMORY、MERGE這四種,四種各有各的好處,技術選型可以針對不同的需求選擇不同的方式。
MyIsAm
MyISAM是MySQL的默認存儲引擎。MyISAM不支持事務、也不支持外鍵,但其訪問速度快,對事務完整性沒有要求。
MyISAM表還支持3中不同的存儲格式
靜態表
靜態表是默認的存儲格式,靜態表中的字段都是非變長的字段,
優點是:存儲非常迅速,容易緩存,出現故障容易恢復;
缺點是:佔用的空間通常比動態表多。(注意: 在存儲時,列的寬度不足時,用空格補足,當時在訪問的時候並不會得到這些空格)
動態表
動態表的字段是變長的,優點是:佔用的空間相對較少,但是頻繁地更新刪除記錄會產生碎片,
需要定期改善性能,並且出現故障的時候恢復相對比較困難。
壓縮表
壓縮表佔用磁盤空間小,每個記錄是被單獨壓縮的,所以只有非常小的訪問開支。
InnoDB
InnoDB存儲引擎提供了具有提交、回滾和崩潰恢復能力的事務安全。但是比起MyISAM存儲引擎,
InnoDB寫的處理效率差一些並且會佔用更多的磁盤空間以保留數據和索引。
而且MySQL支持外鍵存儲引擎只有InnoDB,在創建外鍵的時候,
要求附表必須有對應的索引,子表在創建外鍵的時候也會自動創建對應的索引。(被關聯表的外鍵必須是關聯表的主鍵)
InnoDB的理想使用場合:高併發,更新操作比較多的表。需要使用事務的表。對自動災難恢復有要求的表。
MEMORY
MEMORY存儲引擎使用存在內存中的內容來創建表。
每個MEMORY表只實際對應一個磁盤文件。MEMORY類型的表訪問非常得快,因爲它的數據是放在內存中的,並且默認使用HASH索引。
但是一旦服務關閉,表中的數據就會丟失掉。Memory存儲引擎的使用場合,速度要求快的,臨時數據
MERGE
merge存儲引擎是一組MyISAM表的組合,這些MyISAM表結構必須完全相同,MERGE表中並沒有數據,
對MERGE類型的表可以進行查詢、更新、刪除的操作,這些操作實際上是對內部的MyISAM表進行操作。 對於對MERGE表進行的插入操作,是根據INSERT_METHOD子句定義的插入的表,可以有3個不同的值, first和last值使得插入操作被相應的作用在第一個或最後一個表上,不定義這個子句或者爲NO,
表示不能對這個MERGE表進行插入操作。可以對MERGE表進行drop操作,這個操作只是刪除MERGE表的定義, 對內部的表沒有任何影響。MERGE在磁盤上保留2個以MERGE表名開頭文件:.frm文件存儲表的定義; .MRG文件包含組合表的信息,包括MERGE表由哪些表組成,插入數據時的依據。
可以通過修改.MRG文件來修改MERGE表,但是修改後要通過flush table刷新。

Mysql主流的引擎方式就是這4種,可以按照條件來選擇相應的。

修改Mysql數據庫存儲引擎

show engines; #顯示數據庫是否支持InnoDB
更改方式1:修改配置文件my.cnf
打開my.cnf,在[mysqld]最後添加爲上default-storage-engine=InnoDB,重啓數據庫服務,數據庫默認的引擎修改爲InnoDB
更改方式2:在建表的時候指定
create table tableName( id int primary key, name varchar(50) )type=InnoDB;
更改方式3:建表之後修改
alter table tableName ENGINE=InnoDB; #mysql5.0以後用這種方式
alter table tableName type = InnoDB; #mysql5.0之前用這種方式

spring配置事務

這裏列舉兩種方式,一種是aop自動注入的方式,一種是基於註解的方式。

自動注入的方式
在aop橫切的時候,按照規則在需要的地方,注入事務。這樣造成的結果是可能不需要的地方也會被注入。造成資源的浪費。好處是不用忘記,你不用管有沒有地方沒有被事務管理到。
    <!-- 配置數據源 使用的是Druid數據源 -->
    <bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />

        <!-- 初始化連接大小 -->
        <property name="initialSize" value="0" />
        <!-- 連接池最大使用連接數量 -->
        <property name="maxActive" value="20" />

        <!-- 連接池最小空閒 -->
        <property name="minIdle" value="0" />
        <!-- 獲取連接最大等待時間 -->
        <property name="maxWait" value="60000" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
            value="33" />
        <!-- 用來檢測有效sql -->
        <property name="validationQuery" value="${validationQuery}" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <!-- 打開removeAbandoned功能 -->
        <property name="removeAbandoned" value="true" />
        <!-- 1800秒,也就是30分鐘 -->
        <property name="removeAbandonedTimeout" value="1800" />
        <!-- 關閉abanded連接時輸出錯誤日誌 -->
        <property name="logAbandoned" value="true" />
        <!-- 監控數據庫 -->
        <property name="filters" value="mergeStat" />
    </bean>
    <!-- myBatis文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 添加mybatis的配置 -->
        <property name="configLocation" value="classpath:mybatis.xml"/>
        <!-- 自動掃描entity目錄, 省掉Configuration.xml裏的手工配置 -->
        <property name="mapperLocations" value="classpath:com/tanrice/dao/impl/*.xml" />
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.tanrice.dao" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    </bean>

    <!-- 配置事務管理器 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 配置事物 -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="append*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="insert*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="update*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="modify*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="edit*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delete*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="remove*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="repair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delAndRepair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="get*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="find*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="load*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="search*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="datagrid*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
        </tx:attributes>
    </tx:advice>

    <!-- Spring aop事務管理 -->
    <aop:config>
        <aop:pointcut id="transactionPointcut" expression="execution(* com.projectname.service..*Impl.*(..))" />
        <aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
    </aop:config>
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate" scope="prototype">
         <constructor-arg index="0" ref="sqlSessionFactory" /> 
    </bean>
基於註解的方式
基於註解的方式是在需要使用的類或者方法上面加上@Transactional註解。這樣做的好處是減少資源的浪費,不好的地方是有時候會忘記,一旦忘記的話就會造成有些方法沒有事務。
    <!-- 配置數據源 使用的是Druid數據源 -->
    <bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />

        <!-- 初始化連接大小 -->
        <property name="initialSize" value="0" />
        <!-- 連接池最大使用連接數量 -->
        <property name="maxActive" value="20" />

        <!-- 連接池最小空閒 -->
        <property name="minIdle" value="0" />
        <!-- 獲取連接最大等待時間 -->
        <property name="maxWait" value="60000" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
            value="33" />
        <!-- 用來檢測有效sql -->
        <property name="validationQuery" value="${validationQuery}" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <!-- 打開removeAbandoned功能 -->
        <property name="removeAbandoned" value="true" />
        <!-- 1800秒,也就是30分鐘 -->
        <property name="removeAbandonedTimeout" value="1800" />
        <!-- 關閉abanded連接時輸出錯誤日誌 -->
        <property name="logAbandoned" value="true" />
        <!-- 監控數據庫 -->
        <property name="filters" value="mergeStat" />
    </bean>

    <!-- myBatis文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 添加mybatis的配置 -->
        <property name="configLocation" value="classpath:mybatis.xml"/>
        <!-- 自動掃描entity目錄, 省掉Configuration.xml裏的手工配置 -->
        <property name="mapperLocations" value="classpath:com/tanrice/dao/impl/*.xml" />
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.tanrice.dao" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    </bean>

    <!-- 配置事務管理器 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>


    <!-- 註解的方式配置事務 -->
    <!-- 在需要的地方配置 -->
    <tx:annotation-driven transaction-manager="transactionManager"  proxy-target-class="true"/>

    <!-- 註解方式配置事物 -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="append*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="insert*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="update*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="modify*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="edit*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delete*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="remove*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="repair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delAndRepair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="get*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="find*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="load*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="search*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="datagrid*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
        </tx:attributes>
    </tx:advice>
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate" scope="prototype">
         <constructor-arg index="0" ref="sqlSessionFactory" /> 
    </bean>
@Service
@Transactional
public class UserTaskServiceImpl implements UserTaskService{

    @Autowired
    private UserTaskDao userTaskDao;

    @Autowired
    private TaskDao taskDao;

    public boolean updateUserTask(UserTask userTask,int id) {
        boolean flag = false;
        try{
            flag = userTaskDao.saveUserTask(userTask)>0?true:false;
            System.out.println("插入:"+flag);
            flag = taskDao.updateTaskLeftcountById(id)>0?true:false;
            System.out.println("修改:"+flag);
        }catch(Exception e){
            throw new RuntimeException();
        }
        return flag;
    }
}

以上的兩種方式自己考慮使用,我在xml配置文件的註釋寫的比較詳細,就不一點點解釋了。

測試代碼

service方法已經寫了。主要是把mybatis的代碼貼一下。我把mybatis裏面寫的一個SQL語句故意寫錯了。讓它運行的時候出錯。

    <update id="updateTaskLeftcountById">
        <!-- leftcount爲int類型,所以加'aas'的時候會出錯 -->
        update t_tm_task_table set leftcount = leftcount+aas where id = #{id}
    </update>
    @org.junit.Test
    public void testTrx(){
        UserTask userTask = new UserTask();
        userTask.setFullname("wanda");
        userTask.setUserid(10071);
        userTask.setTaskid(10026);
        userTask.setGettime(new Date().getTime());
        userTask.setState(5);

        System.out.println("結果是:"+userTaskService.updateUserTask(userTask, 10026));
    }

注意

在使用事務的過程中要注意幾個點。

  • 爲什麼不在dao層上設置事務
    • 最根本的原因:因爲dao層直接操作數據庫,責任單一無須回滾,成功就是成功,失敗就是失敗。
  • 事務的隔離級別
    • Read uncommitted 讀未提交:我們所說的髒讀,兩個併發的事務,“事務A:領導給singo發工資”、“事務B:singo查詢工資賬戶”,事務B讀取了事務A尚未提交的數據。
    • Read committed 讀提交:我們所說的不可重複讀,兩個併發的事務,“事務A:singo消費”、“事務B:singo的老婆網上轉賬”,事務A事先讀取了數據,事務B緊接了更新了數據,並提交了事務,而事務A再次讀取該數據時,數據已經發生了改變。
    • Repeatable read 重複讀:可以避免不可重複讀。當singo拿着工資卡去消費時,一旦系統開始讀取工資卡信息(即事務開始),singo的老婆就不可能對該記錄進行修改,也就是singo的老婆不能在此時轉賬。MySQL的默認隔離級別就是Repeatable read。
    • Serializable 序列化:Serializable是最高的事務隔離級別,同時代價也花費最高,性能很低,一般很少使用,在該級別下,事務順序執行,不僅可以避免髒讀、不可重複讀,還避免了幻像讀。
    <!-- 註解方式配置事物 -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="append*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="insert*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="update*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="modify*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="edit*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delete*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="remove*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="repair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delAndRepair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="get*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="find*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="load*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="search*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="datagrid*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
        </tx:attributes>
    </tx:advice>

這是我上面配置的事務隔離,<tx:method name="repair" propagation="REQUIRED" isolation="READ_COMMITTED" /> 看isolation的配置項目,這個就是配置的隔離級別

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章