在Spring Boot和MyBatis中,我们可以使用@Transactional注解和乐观锁或悲观锁来实现转账扣款操作。以下是一个使用乐观锁的示例:
首先,我们需要在账户表中添加一个version字段,用于实现乐观锁。
然后,我们创建一个Account实体类:``
public class Account {
private int id;
private int balance;
private int version; // 用于乐观锁
// getters and setters...
}
接着,我们创建一个AccountMapper接口:
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface AccountMapper {
@Select("SELECT * FROM accounts WHERE id = #{id}")
Account findById(@Param("id") int id);
@Update("UPDATE accounts SET balance = balance - #{amount}, version = version + 1 WHERE id = #{id} AND version = #{version}")
int withdraw(@Param("id") int id, @Param("amount") int amount, @Param("version") int version);
@Update("UPDATE accounts SET balance = balance + #{amount} WHERE id = #{id}")
void deposit(@Param("id") int id, @Param("amount") int amount);
}
最后,我们创建一个BankService类,实现转账扣款操作:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BankService {
private final AccountMapper accountMapper;
public BankService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Transactional
public void transfer(int fromId, int toId, int amount) {
Account fromAccount = accountMapper.findById(fromId);
if (fromAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
int updatedRows = accountMapper.withdraw(fromId, amount, fromAccount.getVersion());
if (updatedRows == 0) {
throw new RuntimeException("Concurrent modification detected");
}
accountMapper.deposit(toId, amount);
}
}
在这个例子中,我们使用@Transactional注解来开启事务。在transfer方法中,我们首先检查余额是否足够,然后尝试扣款。如果扣款失败(可能是因为并发修改),我们就抛出一个异常,然后Spring会自动回滚事务。否则,我们就存款,然后Spring会自动提交事务。
注意,这只是一个简单的例子,实际的生产环境可能会更复杂。例如,你可能需要处理更多的异常情况,或者使用悲观锁来处理高并发环境。
在实际的生产环境中,如果扣款失败(可能是因为并发修改),我们通常会尝试重新执行操作,而不是直接抛出异常。这就是所谓的重试机制。
以下是一个使用重试机制的转账扣款操作示例:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BankService {
private final AccountMapper accountMapper;
public BankService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Transactional
public void transfer(int fromId, int toId, int amount) {
for (int i = 0; i < 3; i++) { // 尝试3次
Account fromAccount = accountMapper.findById(fromId);
if (fromAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
int updatedRows = accountMapper.withdraw(fromId, amount, fromAccount.getVersion());
if (updatedRows > 0) {
accountMapper.deposit(toId, amount);
return;
}
}
throw new RuntimeException("Concurrent modification detected");
}
}
在上述的转账代码中,我们可以进行以下几点优化:
-
异常处理:在实际的生产环境中,我们需要处理更多的异常情况,例如数据库连接失败、SQL语句错误等。我们可以定义一个自定义的异常类,用于封装这些异常情况。
-
日志记录:我们可以添加日志记录,用于记录转账操作的详细信息,例如转账金额、转账时间、转账结果等。这对于问题排查和系统监控非常有用。
-
参数校验:我们需要校验输入参数的有效性,例如账户ID是否存在、转账金额是否大于0等。
以下是一个优化后的转账示例:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class BankService {
private final AccountMapper accountMapper;
private static final Logger logger = LoggerFactory.getLogger(BankService.class);
public BankService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Transactional
public void transfer(int fromId, int toId, int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be greater than zero");
}
for (int i = 0; i < 3; i++) { // 尝试3次
Account fromAccount = accountMapper.findById(fromId);
if (fromAccount == null) {
throw new IllegalArgumentException("Account " + fromId + " does not exist");
}
if (fromAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
int updatedRows = accountMapper.withdraw(fromId, amount, fromAccount.getVersion());
if (updatedRows > 0) {
accountMapper.deposit(toId, amount);
logger.info("Transfer successful: {} -> {}, amount: {}", fromId, toId, amount);
return;
}
logger.warn("Concurrent modification detected, retrying...");
}
logger.error("Transfer failed after 3 attempts: {} -> {}, amount: {}", fromId, toId, amount);
throw new RuntimeException("Concurrent modification detected");
}
}
在这个例子中,我们添加了参数校验、日志记录,并且在发生并发修改时,我们会记录一个警告日志,然后重试操作。如果尝试了3次还是失败,我们就记录一个错误日志,然后抛出一个异常。
在Spring Boot和MyBatis中,我们可以使用@Transactional注解和悲观锁来实现转账扣款操作。以下是一个使用悲观锁的示例:
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface AccountMapper {
@Select("SELECT * FROM accounts WHERE id = #{id} FOR UPDATE")
Account findByIdForUpdate(@Param("id") int id);
@Update("UPDATE accounts SET balance = balance - ? WHERE id = ?")
int withdraw(@Param("id") int id, @Param("amount") int amount);
@Update("UPDATE accounts SET balance = balance + ? WHERE id = ?")
void deposit(@Param("id") int id, @Param("amount") int amount);
}
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BankService {
private final AccountMapper accountMapper;
public BankService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Transactional
public void transfer(int fromId, int toId, int amount) {
Account fromAccount = accountMapper.findByIdForUpdate(fromId);
if (fromAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
accountMapper.withdraw(fromId, amount);
accountMapper.deposit(toId, amount);
}
}
在这个例子中,我们使用@Transactional注解来开启事务。在transfer方法中,我们首先获取fromId账户的信息,并对该账户加锁(悲观锁)。然后,我们检查余额是否足够,如果足够,就执行扣款和存款操作。因为我们使用了悲观锁,所以在我们操作fromId账户的过程中,其他事务不能修改该账户,这就保证了转账操作的一致性。
注意,这只是一个简单的例子,实际的生产环境可能会更复杂。例如,你可能需要处理更多的异常情况,或者使用乐观锁来处理高并发环境。
为了避免死锁,一种常见的策略是总是按照相同的顺序锁定资源。例如,我们可以按照账户ID的顺序来锁定账户。这样,上述的情况就不会发生,因为两个事务会按照相同的顺序来锁定账户A和账户B,不会形成死锁。这句话能对上述代码优化实现下吗
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BankService {
private final AccountMapper accountMapper;
public BankService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Transactional
public void transfer(int fromId, int toId, int amount) {
// 按照账户ID的顺序来锁定账户
int minId = Math.min(fromId, toId);
int maxId = Math.max(fromId, toId);
Account minAccount = accountMapper.findByIdForUpdate(minId);
if (minId == fromId && minAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
Account maxAccount = accountMapper.findByIdForUpdate(maxId);
if (maxId == fromId && maxAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
if (minId == fromId) {
accountMapper.withdraw(fromId, amount);
accountMapper.deposit(toId, amount);
} else {
accountMapper.withdraw(toId, amount);
accountMapper.deposit(fromId, amount);
}
}
}
在实际的生产环境中,数据库通常会提供死锁检测和解决的机制。例如,如果数据库检测到死锁,它可能会自动终止其中一个事务,从而解除死锁。然后,应用程序可以捕获到这个异常,然后重新尝试事务。这句话代码实现下
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.dao.DeadlockLoserDataAccessException;
@Service
public class BankService {
private final AccountMapper accountMapper;
public BankService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
public void transfer(int fromId, int toId, int amount) {
while (true) {
try {
doTransfer(fromId, toId, amount);
break; // 如果转账成功,跳出循环
} catch (DeadlockLoserDataAccessException e) {
// 如果发生死锁,重新尝试转账
}
}
}
@Transactional
protected void doTransfer(int fromId, int toId, int amount) {
// 按照账户ID的顺序来锁定账户
int minId = Math.min(fromId, toId);
int maxId = Math.max(fromId, toId);
Account minAccount = accountMapper.findByIdForUpdate(minId);
if (minId == fromId && minAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
Account maxAccount = accountMapper.findByIdForUpdate(maxId);
if (maxId == fromId && maxAccount.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
if (minId == fromId) {
accountMapper.withdraw(fromId, amount);
accountMapper.deposit(toId, amount);
} else {
accountMapper.withdraw(toId, amount);
accountMapper.deposit(fromId, amount);
}
}
}
参考http://t.csdn.cn/PCBmA参考