SpringBoot 는 MyBatis 를 통합 하여 낙관적 인 자물쇠 와 비관 적 인 자 물 쇠 를 실현 하 는 예 입 니 다.
12565 단어 SpringBootMyBatis낙관적 자물쇠비관 적 자물쇠
모든 코드:https://github.com/imcloudfloating/Lock_Demo
GitHub Page: https://cloudli.top
질문
A,B 두 계좌 가 동시에 상대방 에 게 이 체 를 할 때 다음 과 같은 상황 이 발생 한다.
시시각각
사무 1(A 가 B 에 게 이체)
사무 2(B 가 A 에 게 이체)
T1
Lock A
Lock B
T2
Lock B(사무 2 로 인해 A 잠 금,대기)
Lock A(사무 1 이 잠 겨 있 기 때문에 B,대기)
두 가지 업무 가 모두 상대방 이 자 물 쇠 를 풀 기 를 기다 리 고 있 기 때문에 자물쇠 가 생 겼 습 니 다.해결 방안 은 홈 키 의 크기 에 따라 자 물 쇠 를 추가 하고 항상 홈 키 가 작 거나 큰 줄 의 데 이 터 를 잠 그 는 것 입 니 다.
데이터 시트 만 들 고 데이터 삽입(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;
}
AccountServicetransferOptimistic 방법 에 사용자 정의 주석@Retry 가 있 습 니 다.이것 은 낙관적 인 잠 금 이 실 패 했 을 때 다시 시도 합 니 다.
@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;
/**
* ,
*
* @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;
}
/**
* ,
* @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) {
// , ,
throw new RetryException("Transfer failed, retry.");
} else {
return Result.SUCCESS;
}
}
}
Spring AOP 를 사용 하여 낙관적 인 잠 금 을 실현 하 는 데 실패 한 후 다시 시도 합 니 다.사용자 정의 주석 Retry
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
int value() default 3; //
}
다시 시도 이상 RetryException
public class RetryException extends RuntimeException {
public RetryException(String message) {
super(message);
}
}
다시 시도 하 는 절단면 류try Again 방법 은@Around 주석(서 라운드 알림 표시)을 사용 하여 대상 방법 이 언제 실 행 될 지,실행 하지 않 을 지,결 과 를 사용자 정의 할 수 있 습 니 다.여기 서 먼저 Proceeding JoinPoint.proceed()방법 을 통 해 목표 방법 을 수행 합 니 다.재 시도 이상 을 던 지면 만 3 회 까지 다시 실행 하고 세 번 성공 하지 못 하면 다시 굴 러 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);
}
}
유닛 테스트여러 스 레 드 로 동시 이체 시 뮬 레이 션 을 하고 테스트 를 통 해 비관 적 인 자 물 쇠 는 계 정 잔액 이 부족 하거나 데이터 베이스 연결 이 부족 하고 시간 이 초과 되 는 것 을 제외 하고 모두 성공 했다.낙관적 인 자 물 쇠 는 재 시도 에 도 성공 하 는 스 레 드 가 적 고 500 개 평균 10 여 개 에 성공 한다.
따라서 많이 읽 고 적 게 쓰 는 조작 에 대해 비관 적 인 자 물 쇠 를 사용 하고 많이 읽 고 적 게 쓰 는 조작 에 대해 낙관적 인 자 물 쇠 를 사용 할 수 있다.
전체 코드 는 Github:https://github.com/imcloudfloating/Lock_Demo을 보십시오.
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
class AccountServiceTest {
//
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));
}
/**
*
*/
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
이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
【Java・SpringBoot・Thymeleaf】 에러 메세지를 구현(SpringBoot 어플리케이션 실천편 3)로그인하여 사용자 목록을 표시하는 응용 프로그램을 만들고, Spring에서의 개발에 대해 공부하겠습니다 🌟 마지막 데이터 바인딩에 계속 바인딩 실패 시 오류 메시지를 구현합니다. 마지막 기사🌟 src/main/res...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.