Spring框架數(shù)據庫事務詳解

Spring框架提供了對數(shù)據庫事務的支持,它可以很方便地實現(xiàn)聲明式的事務管理。Spring框架的事務管理主要是通過AOP實現(xiàn)的,在方法執(zhí)行前后增加了事務的開啟、提交或回滾操作。在這里,我們將講解Spring框架的數(shù)據庫事務原理以及事務隔離級別,并提供一些相關的代碼示例。

Spring框架的事務管理原理

Spring框架的事務管理基于AOP實現(xiàn),它提供了一個事務管理器接口——PlatformTransactionManager,它定義了事務管理器的基本行為。Spring框架還提供了一個基于注解的事務管理器——@Transactional,它可以很方便地對方法進行事務管理。

在Spring框架中,事務的開啟、提交或回滾是通過AOP的Around Advice來實現(xiàn)的。當一個被事務管理器管理的方法被調用時,AOP會攔截這個方法的調用,并執(zhí)行如下操作:

開啟一個新的事務。

執(zhí)行方法體。

如果方法執(zhí)行成功,則提交事務;否則,回滾事務。

以下是一個簡單的示例,演示了如何使用Spring框架來實現(xiàn)聲明式的事務管理:

javaCopy code

@Service

public class UserServiceImpl implements UserService {

? ? @Autowired

? ? private UserDao userDao;

? ? @Override

? ? @Transactional

? ? public void transferMoney(String fromUser, String toUser, double amount) {

? ? ? ? // 從fromUser賬戶中減去指定的金額

? ? ? ? userDao.updateBalance(fromUser, -amount);

? ? ? ? // 向toUser賬戶中加上指定的金額

? ? ? ? userDao.updateBalance(toUser, amount);

? ? }

}

在這個示例中,@Transactional注解標記在transferMoney方法上,表示這個方法需要進行事務管理。如果這個方法執(zhí)行成功,事務將被提交;否則,事務將被回滾。

Spring框架的事務隔離級別

Spring框架支持以下5種事務隔離級別:

DEFAULT:使用數(shù)據庫默認的事務隔離級別。

READ_UNCOMMITTED:允許讀取未提交的數(shù)據。

READ_COMMITTED:只能讀取已經提交的數(shù)據。

REPEATABLE_READ:在同一個事務中多次讀取同一個數(shù)據時,讀取的數(shù)據保持一致。

SERIALIZABLE:事務串行化執(zhí)行,可以避免臟讀、不可重復讀和幻讀問題。

Spring框架默認使用數(shù)據庫的默認事務隔離級別。在大多數(shù)情況下,使用READ_COMMITTED隔離級別即可。

接下來,我們將結合代碼示例來演示 Spring 框架中事務管理的實現(xiàn)過程以及不同隔離級別的應用。

首先,我們需要在 Spring 配置文件中開啟事務管理器:

xmlCopy code

<bean id="transactionManager"

? ? ? class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

? <property name="dataSource" ref="dataSource" />

</bean>

<tx:annotation-driven transaction-manager="transactionManager" />

接下來,在我們需要使用事務管理的 Service 層方法上添加?@Transactional?注解,該注解可用于類或方法上,用于指定事務的隔離級別、傳播行為等屬性。

javaCopy code

@Service

public class UserServiceImpl implements UserService {

? ? @Autowired

? ? private UserDao userDao;

? ? @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)

? ? public void transferMoney(int fromId, int toId, int money) {

? ? ? ? userDao.reduceMoney(fromId, money);

? ? ? ? userDao.addMoney(toId, money);

? ? }

}

在上述示例代碼中,我們在?transferMoney()?方法上添加了?@Transactional?注解,并指定了事務的隔離級別為?READ_COMMITTED,傳播行為為?REQUIRED,表示該方法執(zhí)行時需要一個已存在的事務,若不存在則新建一個事務。

最后,我們需要在 Spring 配置文件中配置數(shù)據源相關信息:

xmlCopy code

<bean id="dataSource"

? ? ? class="org.springframework.jdbc.datasource.DriverManagerDataSource">

? <property name="driverClassName" value="com.mysql.jdbc.Driver" />

? <property name="url" value="jdbc:mysql://localhost:3306/test" />

? <property name="username" value="root" />

? <property name="password" value="password" />

</bean>

上述配置中,我們配置了連接數(shù)據庫的相關信息,這里使用的是?DriverManagerDataSource,也可以使用其他的數(shù)據源,例如?C3P0?等。

在實際開發(fā)過程中,我們需要根據具體業(yè)務需求來選擇合適的隔離級別,并根據具體場景來確定事務的傳播行為,以避免出現(xiàn)數(shù)據不一致的問題。

需要注意的是,隔離級別越高,對數(shù)據庫性能的影響越大,因此需要根據實際需求來進行平衡,選擇合適的隔離級別。

提供幾個不同隔離級別的示例代碼進行講解。

首先,我們需要在 Spring 配置文件中啟用事務管理,例如使用聲明式事務管理的方式,可以通過配置?TransactionManager?和?TransactionInterceptor?來實現(xiàn):

xmlCopy code

<!-- 配置事務管理器 -->

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

? ? <property name="dataSource" ref="dataSource" />

</bean>

<!-- 配置事務攔截器 -->

<tx:advice id="txAdvice" transaction-manager="transactionManager">

? ? <tx:attributes>

? ? ? ? <tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" rollback-for="Exception" />

? ? </tx:attributes>

</tx:advice>

<!-- 配置 AOP 自動代理 -->

<aop:config>

? ? <aop:pointcut id="serviceMethod" expression="execution(* com.example.service.*.*(..))" />

? ? <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod" />

</aop:config>

上述配置文件中,我們使用了?DataSourceTransactionManager?作為事務管理器,并配置了?TransactionInterceptor?作為事務攔截器,同時將?TransactionInterceptor?應用到了所有在?com.example.service?包下的 Service 層方法中。

接下來,我們分別來講解下不同的事務隔離級別的代碼實現(xiàn):

讀未提交(READ_UNCOMMITTED)

在該隔離級別下,一個事務可以讀取另一個事務未提交的數(shù)據,因此可能會出現(xiàn)臟讀的問題。下面是一個簡單的示例代碼:

javaCopy code

@Service

public class UserServiceImpl implements UserService {


? ? @Autowired

? ? private JdbcTemplate jdbcTemplate;


? ? @Override

? ? @Transactional(isolation = Isolation.READ_UNCOMMITTED)

? ? public User getUserById(int id) {

? ? ? ? String sql = "SELECT * FROM users WHERE id = ?";

? ? ? ? RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);

? ? ? ? return jdbcTemplate.queryForObject(sql, rowMapper, id);

? ? }


? ? @Override

? ? @Transactional(propagation = Propagation.REQUIRED)

? ? public void updateUser(int id, String name) {

? ? ? ? String sql = "UPDATE users SET name = ? WHERE id = ?";

? ? ? ? jdbcTemplate.update(sql, name, id);

? ? }

}

上述代碼中,getUserById?方法的隔離級別為?READ_UNCOMMITTED,可以讀取其他事務未提交的數(shù)據,因此可能會讀取到臟數(shù)據;updateUser?方法的隔離級別為默認值?READ_COMMITTED,因此修改操作會在提交前進行排他鎖,避免了臟讀的問題。

2.讀已提交(READ_COMMITTED)

在該隔離級別下,一個事務只能讀取另一個事務已提交的數(shù)據,因此避免了臟讀的問題。下面是一個簡單的示例代碼:

javaCopy code

@Service

public class UserService {

? ? @Autowired

? ? private JdbcTemplate jdbcTemplate;

? ? @Transactional(isolation = Isolation.READ_COMMITTED)

? ? public void updateUserBalance(Long userId, int newBalance) {

? ? ? ? String sql = "UPDATE user SET balance = ? WHERE id = ?";

? ? ? ? jdbcTemplate.update(sql, newBalance, userId);

? ? }

? ? @Transactional(isolation = Isolation.READ_COMMITTED)

? ? public int getUserBalance(Long userId) {

? ? ? ? String sql = "SELECT balance FROM user WHERE id = ?";

? ? ? ? Integer balance = jdbcTemplate.queryForObject(sql, Integer.class, userId);

? ? ? ? return balance == null ? 0 : balance;

? ? }

}

這里的?@Transactional?注解指定了事務隔離級別為?Isolation.READ_COMMITTED,表示讀已提交。我們來看一下在這個隔離級別下會有什么效果。

假設有兩個線程同時調用?updateUserBalance?和?getUserBalance?方法,其中一個線程執(zhí)行?updateUserBalance?方法更新了用戶余額,而另一個線程在此同時執(zhí)行?getUserBalance?方法查詢該用戶的余額。那么,在讀已提交隔離級別下,第二個線程只能讀到已經提交的數(shù)據,也就是更新后的余額。這就保證了數(shù)據的一致性,避免了臟讀和不可重復讀。

3.REPEATABLE_READ

對于可重復讀隔離級別,我們可以在?@Transactional?注解中指定?isolation?屬性為?Isolation.REPEATABLE_READ,表示使用可重復讀隔離級別。

下面是一個使用 Spring 框架的可重復讀隔離級別的代碼示例:

javaCopy code

@Service

public class UserService {

? ? @Autowired

? ? private JdbcTemplate jdbcTemplate;

? ? @Transactional(isolation = Isolation.REPEATABLE_READ)

? ? public void transfer(int fromUserId, int toUserId, int amount) {

? ? ? ? try {

? ? ? ? ? ? jdbcTemplate.update("UPDATE user SET balance = balance - ? WHERE id = ?", amount, fromUserId);

? ? ? ? ? ? jdbcTemplate.update("UPDATE user SET balance = balance + ? WHERE id = ?", amount, toUserId);

? ? ? ? ? ? Thread.sleep(10000); // 模擬業(yè)務處理時間

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? }

? ? @Transactional(isolation = Isolation.REPEATABLE_READ)

? ? public void showBalance(int userId) {

? ? ? ? int balance = jdbcTemplate.queryForObject("SELECT balance FROM user WHERE id = ?", new Object[]{userId}, Integer.class);

? ? ? ? System.out.println("User " + userId + " balance: " + balance);

? ? }

}

在上面的示例中,我們定義了一個?UserService?類,其中有兩個方法:transfer?和?showBalance。

transfer?方法用于轉賬操作。在方法上使用?@Transactional?注解,并指定?isolation?屬性為?Isolation.REPEATABLE_READ,表示使用可重復讀隔離級別。在該方法中,我們先更新轉出用戶的余額,再更新轉入用戶的余額,并模擬業(yè)務處理時間。

showBalance?方法用于查詢用戶的余額。同樣在方法上使用?@Transactional?注解,并指定?isolation?屬性為?Isolation.REPEATABLE_READ。在該方法中,我們查詢指定用戶的余額,并輸出到控制臺。

我們可以在一個單元測試中測試這兩個方法:

javaCopy code

@RunWith(SpringRunner.class)

@SpringBootTest

public class UserServiceTest {

? ? @Autowired

? ? private UserService userService;

? ? @Test

? ? public void testTransfer() throws InterruptedException {

? ? ? ? userService.transfer(1, 2, 100);

? ? }

? ? @Test

? ? public void testShowBalance() {

? ? ? ? userService.showBalance(1);

? ? ? ? userService.showBalance(2);

? ? }

}

在上面的測試中,我們先調用?transfer?方法進行轉賬操作,模擬一個并發(fā)訪問的場景;然后調用?showBalance?方法查詢用戶余額。

4.SERIALIZABLE 隔離級別

SERIALIZABLE 是最高的隔離級別,它保證了所有事務的串行執(zhí)行。在這種隔離級別下,所有事務都必須先獲得鎖才能執(zhí)行任何操作,這樣可以避免所有的并發(fā)問題。

我們可以使用以下的示例代碼來演示 SERIALIZABLE 隔離級別:

javaCopy code

@Service

public class BankService {

? ? @Autowired

? ? private JdbcTemplate jdbcTemplate;

? ? @Transactional(isolation = Isolation.SERIALIZABLE)

? ? public void transfer(int fromId, int toId, int amount) {

? ? ? ? jdbcTemplate.update("SELECT balance FROM accounts WHERE id = ? FOR UPDATE", fromId);

? ? ? ? int fromBalance = jdbcTemplate.queryForObject("SELECT balance FROM accounts WHERE id = ?", Integer.class, fromId);

? ? ? ? if (fromBalance < amount) {

? ? ? ? ? ? throw new RuntimeException("Insufficient balance");

? ? ? ? }

? ? ? ? jdbcTemplate.update("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromId);

? ? ? ? jdbcTemplate.update("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toId);

? ? }

}

這里我們使用了?@Transactional?注解來聲明一個事務,同時指定了隔離級別為 SERIALIZABLE。在轉賬過程中,我們先對要轉賬的賬戶加鎖,然后再執(zhí)行轉賬操作。

值得注意的是,在 SERIALIZABLE 隔離級別下,所有的事務都必須先獲得鎖才能執(zhí)行任何操作。因此,在多個事務同時執(zhí)行的情況下,可能會導致死鎖的問題。因此,在使用 SERIALIZABLE 隔離級別時,我們需要特別小心,確保不會發(fā)生死鎖。

希望這些示例能幫助您更好地理解 Spring 框架中的事務管理機制。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容