SpringBoot整合MyBatis實現(xiàn)樂觀鎖和悲觀鎖的示例
本文以轉(zhuǎn)賬操作為例,實現(xiàn)并測試樂觀鎖和悲觀鎖。
全部代碼:https://github.com/imcloudfloating/Lock_Demo
GitHub Page:https://cloudli.top
死鎖問題
當 A, B 兩個賬戶同時向?qū)Ψ睫D(zhuǎn)賬時,會出現(xiàn)如下情況:
| 時刻 | 事務(wù) 1 (A 向 B 轉(zhuǎn)賬) | 事務(wù) 2 (B 向 A 轉(zhuǎn)賬) |
|---|---|---|
| T1 | Lock A | Lock B |
| T2 | Lock B (由于事務(wù) 2 已經(jīng) Lock A,等待) | Lock A (由于事務(wù) 1 已經(jīng) Lock B,等待) |
由于兩個事務(wù)都在等待對方釋放鎖,于是死鎖產(chǎn)生了,解決方案:按照主鍵的大小來加鎖,總是先鎖主鍵較小或較大的那行數(shù)據(jù)。
建立數(shù)據(jù)表并插入數(shù)據(jù)(MySQL)
create table account
(
id int auto_increment
primary key,
deposit decimal(10, 2) default 0.00 not null,
version int default 0 not null
);
INSERT INTO vault.account (id, deposit, version) VALUES (1, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (2, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (3, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (4, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (5, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (6, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (7, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (8, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (9, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (10, 1000, 0);
Mapper 文件
悲觀鎖使用 select ... for update,樂觀鎖使用 version 字段。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cloud.demo.mapper.AccountMapper">
<select id="selectById" resultType="com.cloud.demo.model.Account">
select *
from account
where id = #{id}
</select>
<update id="updateDeposit" keyProperty="id" parameterType="com.cloud.demo.model.Account">
update account
set deposit=#{deposit},
version = version + 1
where id = #{id}
and version = #{version}
</update>
<select id="selectByIdForUpdate" resultType="com.cloud.demo.model.Account">
select *
from account
where id = #{id} for
update
</select>
<update id="updateDepositPessimistic" keyProperty="id" parameterType="com.cloud.demo.model.Account">
update account
set deposit=#{deposit}
where id = #{id}
</update>
<select id="getTotalDeposit" resultType="java.math.BigDecimal">
select sum(deposit) from account;
</select>
</mapper>
Mapper 接口
@Component
public interface AccountMapper {
Account selectById(int id);
Account selectByIdForUpdate(int id);
int updateDepositWithVersion(Account account);
void updateDeposit(Account account);
BigDecimal getTotalDeposit();
}
Account POJO
@Data
public class Account {
private int id;
private BigDecimal deposit;
private int version;
}
AccountService
在 transferOptimistic 方法上有個自定義注解 @Retry,這個用來實現(xiàn)樂觀鎖失敗后重試。
@Slf4j
@Service
public class AccountService {
public enum Result{
SUCCESS,
DEPOSIT_NOT_ENOUGH,
FAILED,
}
@Resource
private AccountMapper accountMapper;
private BiPredicate<BigDecimal, BigDecimal> isDepositEnough = (deposit, value) -> deposit.compareTo(value) > 0;
/**
* 轉(zhuǎn)賬操作,悲觀鎖
*
* @param fromId 扣款賬戶
* @param toId 收款賬戶
* @param value 金額
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public Result transferPessimistic(int fromId, int toId, BigDecimal value) {
Account from, to;
try {
// 先鎖 id 較大的那行,避免死鎖
if (fromId > toId) {
from = accountMapper.selectByIdForUpdate(fromId);
to = accountMapper.selectByIdForUpdate(toId);
} else {
to = accountMapper.selectByIdForUpdate(toId);
from = accountMapper.selectByIdForUpdate(fromId);
}
} catch (Exception e) {
log.error(e.getMessage());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Result.FAILED;
}
if (!isDepositEnough.test(from.getDeposit(), value)) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.info(String.format("Account %d is not enough.", fromId));
return Result.DEPOSIT_NOT_ENOUGH;
}
from.setDeposit(from.getDeposit().subtract(value));
to.setDeposit(to.getDeposit().add(value));
accountMapper.updateDeposit(from);
accountMapper.updateDeposit(to);
return Result.SUCCESS;
}
/**
* 轉(zhuǎn)賬操作,樂觀鎖
* @param fromId 扣款賬戶
* @param toId 收款賬戶
* @param value 金額
*/
@Retry
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Result transferOptimistic(int fromId, int toId, BigDecimal value) {
Account from = accountMapper.selectById(fromId),
to = accountMapper.selectById(toId);
if (!isDepositEnough.test(from.getDeposit(), value)) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Result.DEPOSIT_NOT_ENOUGH;
}
from.setDeposit(from.getDeposit().subtract(value));
to.setDeposit(to.getDeposit().add(value));
int r1, r2;
// 先鎖 id 較大的那行,避免死鎖
if (from.getId() > to.getId()) {
r1 = accountMapper.updateDepositWithVersion(from);
r2 = accountMapper.updateDepositWithVersion(to);
} else {
r2 = accountMapper.updateDepositWithVersion(to);
r1 = accountMapper.updateDepositWithVersion(from);
}
if (r1 < 1 || r2 < 1) {
// 失敗,拋出重試異常,執(zhí)行重試
throw new RetryException("Transfer failed, retry.");
} else {
return Result.SUCCESS;
}
}
}
使用 Spring AOP 實現(xiàn)樂觀鎖失敗后重試
自定義注解 Retry
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
int value() default 3; // 重試次數(shù)
}
重試異常 RetryException
public class RetryException extends RuntimeException {
public RetryException(String message) {
super(message);
}
}
重試的切面類
tryAgain 方法使用了 @Around 注解(表示環(huán)繞通知),可以決定目標方法在何時執(zhí)行,或者不執(zhí)行,以及自定義返回結(jié)果。這里首先通過 ProceedingJoinPoint.proceed() 方法執(zhí)行目標方法,如果拋出了重試異常,那么重新執(zhí)行直到滿三次,三次都不成功則回滾并返回 FAILED。
@Slf4j
@Aspect
@Component
public class RetryAspect {
@Pointcut("@annotation(com.cloud.demo.annotation.Retry)")
public void retryPointcut() {
}
@Around("retryPointcut() && @annotation(retry)")
@Transactional(isolation = Isolation.READ_COMMITTED)
public Object tryAgain(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
int count = 0;
do {
count++;
try {
return joinPoint.proceed();
} catch (RetryException e) {
if (count > retry.value()) {
log.error("Retry failed!");
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return AccountService.Result.FAILED;
}
}
} while (true);
}
}
單元測試
用多個線程模擬并發(fā)轉(zhuǎn)賬,經(jīng)過測試,悲觀鎖除了賬戶余額不足,或者數(shù)據(jù)庫連接不夠以及等待超時,全部成功;樂觀鎖即使加了重試,成功的線程也很少,500 個平均也就十幾個成功。
所以對于寫多讀少的操作,使用悲觀鎖,對于讀多寫少的操作,可以使用樂觀鎖。
完整代碼請見 Github:https://github.com/imcloudfloating/Lock_Demo。
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
class AccountServiceTest {
// 并發(fā)數(shù)
private static final int COUNT = 500;
@Resource
AccountMapper accountMapper;
@Resource
AccountService accountService;
private CountDownLatch latch = new CountDownLatch(COUNT);
private List<Thread> transferThreads = new ArrayList<>();
private List<Pair<Integer, Integer>> transferAccounts = new ArrayList<>();
@BeforeEach
void setUp() {
Random random = new Random(currentTimeMillis());
transferThreads.clear();
transferAccounts.clear();
for (int i = 0; i < COUNT; i++) {
int from = random.nextInt(10) + 1;
int to;
do{
to = random.nextInt(10) + 1;
} while (from == to);
transferAccounts.add(new Pair<>(from, to));
}
}
/**
* 測試悲觀鎖
*/
@Test
void transferByPessimisticLock() throws Throwable {
for (int i = 0; i < COUNT; i++) {
transferThreads.add(new Transfer(i, true));
}
for (Thread t : transferThreads) {
t.start();
}
latch.await();
Assertions.assertEquals(accountMapper.getTotalDeposit(),
BigDecimal.valueOf(10000).setScale(2, RoundingMode.HALF_UP));
}
/**
* 測試樂觀鎖
*/
@Test
void transferByOptimisticLock() throws Throwable {
for (int i = 0; i < COUNT; i++) {
transferThreads.add(new Transfer(i, false));
}
for (Thread t : transferThreads) {
t.start();
}
latch.await();
Assertions.assertEquals(accountMapper.getTotalDeposit(),
BigDecimal.valueOf(10000).setScale(2, RoundingMode.HALF_UP));
}
/**
* 轉(zhuǎn)賬線程
*/
class Transfer extends Thread {
int index;
boolean isPessimistic;
Transfer(int i, boolean b) {
index = i;
isPessimistic = b;
}
@Override
public void run() {
BigDecimal value = BigDecimal.valueOf(
new Random(currentTimeMillis()).nextFloat() * 100
).setScale(2, RoundingMode.HALF_UP);
AccountService.Result result = AccountService.Result.FAILED;
int fromId = transferAccounts.get(index).getKey(),
toId = transferAccounts.get(index).getValue();
try {
if (isPessimistic) {
result = accountService.transferPessimistic(fromId, toId, value);
} else {
result = accountService.transferOptimistic(fromId, toId, value);
}
} catch (Exception e) {
log.error(e.getMessage());
} finally {
if (result == AccountService.Result.SUCCESS) {
log.info(String.format("Transfer %f from %d to %d success", value, fromId, toId));
}
latch.countDown();
}
}
}
}
MySQL 配置
innodb_rollback_on_timeout='ON' max_connections=1000 innodb_lock_wait_timeout=500
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java Base64算法實際應(yīng)用之郵件發(fā)送實例分析
這篇文章主要介紹了Java Base64算法實際應(yīng)用之郵件發(fā)送,結(jié)合實例形式分析了java字符編碼與郵件發(fā)送相關(guān)操作技巧,需要的朋友可以參考下2019-09-09
idea maven 構(gòu)建本地jar包及pom文件的過程
這篇文章主要介紹了idea maven 構(gòu)建本地jar包及pom文件的過程,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-11-11
Java Web應(yīng)用程序?qū)崿F(xiàn)基礎(chǔ)的文件下載功能的實例講解
這里我們演示了Servelet驅(qū)動Tomcat來進行HTTP下載的方法,接下來就詳細來看Java Web應(yīng)用程序?qū)崿F(xiàn)基礎(chǔ)的文件下載功能的實例講解2016-05-05
java返回前端實體類json數(shù)據(jù)時忽略某個屬性方法
這篇文章主要給大家介紹了關(guān)于java返回前端實體類json數(shù)據(jù)時忽略某個屬性的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下2023-08-08

