rain2005 写道
楼主的代码是没有问题的,其实我想表达的意思就是楼主的测试并发大时必然死锁,从楼主的标题看是想测试spring事务的并发,楼主完全可以这样
线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试spring并发的事务正确性。
如果想测试程序的健壮性,如死锁可以再写测试用例。
总之保证,每个测试用例目标明确。
好了!花了点时间,完善了我对上述
rain2005 所臆想场景的模拟,并证明了此提议的荒谬。
我先给原本的测试类做了些必要的修改,然后为其添加子类 AccountTransferMultiThreadTestAccountsNotConflict。
目的:让每一个转帐线程所选取的转出(from)与转入(to)户头
不与其它线程相冲突。也就是说,模拟了场景:
“楼主完全可以这样 线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试spring并发的事务正确性。”,并证明其提议之荒谬:在这种“井水不犯河水”的操作下,根本测不了什么“spring并发的事务正确性”。
结果:在打开 spring 声明式事务处理的情况下,父测试类 AccountTransferMultiThreadTest(各转帐线程选取的户头出现冲突)与本测试类 AccountTransferMultiThreadTestAccountsNotConflict(各转帐线程选取的户头没有任何冲突)
均顺利通过,junit 显示绿色条;
在关闭 spring 声明式事务处理的情况下,父测试类 AccountTransferMultiThreadTest(各转帐线程选取的户头出现冲突)
失败,junit 显示红色条。从错误信息发现,转帐前后,所有账户总额不一致;
而在同样关闭 spring 声明式事务处理的情况下,子测试类 AccountTransferMultiThreadTestAccountsNotConflict(各转帐线程选取的户头没有任何冲突)
仍然顺利通过,junit 显示绿色条。从打印信息发现,转帐前后,所有账户总额保持一致,并且每个账户的最终余额和记录(balanceTracking)中完全相同;
证明:AccountTransferMultiThreadTestAccountsNotConflict 所模拟的这种(户头没有冲突的)操作
完全测试不了“spring并发的事务正确性”,rain2005 的提案没有任何意义;而我在顶楼(当时有小错误,后来已修正)所提出的做法才能真正达到这个目的。
希望大家在发贴的时候要谨慎些。不管你自己有多菜,总有比你更菜的人。你那些不负责任的论断,极有可能给他们造成误导。
附代码:
子测试类:AccountTransferMultiThreadTestAccountsNotConflict.java:
//import 省略
/**
*
* 测试类 AccountTransferMultiThreadTestAccountsNotConflict 继承了
* 测试类 AccountTransferMultiThreadTest。
*
* 目的:让每一个转帐线程所选取的转出(from)与转入(to)户头不与其它线程相冲突。也就是说,
* 模拟了 iteye.com 中某某人所臆想的场景:<b>“楼主完全可以这样 线程1操作帐户A,B,
* 线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试spring并发的事务正确性。”</b>,
* 并证明他的提议之荒谬:在这种“井水不犯河水”的操作下,根本测不了什么“spring并发的事务正确性”。
*/
public class AccountTransferMultiThreadTestAccountsNotConflict extends
AccountTransferMultiThreadTest {
private LinkedList<Long> accountIdsNotChosen;
public AccountTransferMultiThreadTestAccountsNotConflict() {
super();
// 重新设置父类中定义的 测试户头的总数 和 测试线程总数。
numOfAccounts = 200; // 测试户头的总数。这里,它必须是偶数。
numOfTransfers = 100; // 测试线程总数(即转帐总次数。这里,它必须等于 测试户头总数 的一半。)
}
protected void setUp() throws Exception {
super.setUp();
// 利用“accountIdsNotChosen”,避免重复选取户头。
accountIdsNotChosen = new LinkedList<Long>();
for (Long id : accountIds) {
accountIdsNotChosen.add(id);
}
}
protected void tearDown() throws Exception {
super.tearDown();
}
/* (non-Javadoc)
* @see xiao.test.spring.testCases.AccountTransferMultiThreadTest#generateTransferThread()
*/
@Override
protected TransferThread generateTransferThread() {
return new TransferThreadAccountsNotConflict(accountService, accountIds,
accountIdsNotChosen, balanceTracking);
}
/* (non-Javadoc)
* @see xiao.test.spring.testCases.AccountTransferMultiThreadTest#testMultiThreadTransfer()
*/
@Override
public void testMultiThreadTransfer() throws Throwable {
super.testMultiThreadTransfer();
}
private static class TransferThreadAccountsNotConflict extends TransferThread {
private LinkedList<Long> accountIdsNotChosen;
public TransferThreadAccountsNotConflict(AccountService accountService,
long[] accountIds, LinkedList<Long> accountIdsNotChosen, Map<Long, BigDecimal> balanceTracking) {
super(accountService, accountIds, balanceTracking);
this.accountIdsNotChosen = accountIdsNotChosen;
if (accountIdsNotChosen.size() <= 1) {
throw new AppException("There are at most 1 account in 'not chosen list', cannot"
+ " choose 2 accounts to make a transfer!");
}
}
/* (non-Javadoc)
* @see xiao.test.spring.testCases.AccountTransferMultiThreadTest.TransferThread#generateTransferOptions()
*/
@Override
protected void generateTransferOptions() {
Random randomGenerator = new Random();
synchronized (accountIdsNotChosen) {
// 随机选取转出户头
int i = randomGenerator.nextInt(accountIdsNotChosen.size());
fromId = accountIdsNotChosen.remove(i);
// 随机选取转入户头
i = randomGenerator.nextInt(accountIdsNotChosen.size());
toId = accountIdsNotChosen.remove(i);
}
// 随机选取转帐数额(0 ~ 149元之间)
amount = BigDecimal.valueOf(randomGenerator.nextInt(150));
}
/* (non-Javadoc)
* @see xiao.test.spring.testCases.AccountTransferMultiThreadTest.TransferThread#runTest()
*/
@Override
public void runTest() throws Throwable {
super.runTest();
}
}
}
原测试类 AccountTransferMultiThreadTest.java,已做必要修改:
//import 省略
/**
* 测试类 AccountTransferMultiThreadTest,使用了 groboutils 以实现多线程测试。
*
* 每个测试线程从一定数量的测试户头中随机选取一对 转出/转入 户头,然后进行一次随机数额的转帐。
*
* 测试户头总数由常量 numOfAccounts 设定。
*
* 测试线程总数由常量 numOfTransfers 设定。
*
*/
public class AccountTransferMultiThreadTest extends TestCase {
// 每个测试户头的初始余额为1000元
private static final BigDecimal INIT_BALANCE = BigDecimal.valueOf(100000L, 2);
private static int successTransfers = 0;
protected int numOfAccounts; // 测试户头的总数
protected int numOfTransfers; // 测试线程总数(即转帐总次数)
private ApplicationContext context;
protected AccountService accountService;
protected long[] accountIds;
protected Map<Long, BigDecimal> balanceTracking = new HashMap<Long, BigDecimal>();;
public AccountTransferMultiThreadTest() {
super();
numOfAccounts = 10; // 测试户头的总数
numOfTransfers = 300; // 测试线程总数(即转帐总次数)
context = new ClassPathXmlApplicationContext("xiao/test/spring/*Context.xml");
accountService = (AccountService) context.getBean("accountService");
}
/* (non-Javadoc)
* @see junit.framework.TestCase#setUp()
*
* 在setUp方法中,生成测试所需的Spring Application Context, 并在数据库中创建
* 一定数量的户头(Account),供多线程测试使用。
*
*/
protected void setUp() throws Exception {
super.setUp();
Account[] accounts = new Account[numOfAccounts];
accountIds = new long[accounts.length];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new Account();
accounts[i].setBalance(INIT_BALANCE);
// 将当前生成的户头写入数据库
accountService.create(accounts[i]);
// 重要步骤!将当前生成的户头主键记录下来,以供测试线程使用
accountIds[i] = (Long)accounts[i].getId();
}
}
/* (non-Javadoc)
* @see junit.framework.TestCase#tearDown()
*/
protected void tearDown() throws Exception {
super.tearDown();
}
protected Account[] getAccounts() {
Account[] accounts = new Account[accountIds.length];
for (int i = 0; i < accountIds.length; i++) {
// 从数据库获取这个户头对象
accounts[i] = accountService.findById(accountIds[i]);
}
// 返回户头数组
return accounts;
}
protected TransferThread generateTransferThread() {
return new TransferThread(accountService, accountIds, balanceTracking);
}
public void testMultiThreadTransfer() throws Throwable {
// 验证在仅有一级缓存的情况下,用同样的主键交给 accountService.findById,它每次
// 返回的是相等,但并不同一的实例。
// 当然了,Account 对象的 equals 必须被正确的覆盖先。
for (int i = 0; i < accountIds.length; i++) {
assertEquals(accountService.findById(accountIds[i]),
accountService.findById(accountIds[i]));
assertNotSame(accountService.findById(accountIds[i]),
accountService.findById(accountIds[i]));
}
// 获取户头对象数组
Account[] accounts = getAccounts();
//System.out.printf("Starting %s transfers...\n", numOfTransfers);
// 记录测试前的所有户头总余额
BigDecimal total1 = accountService.getTotalBalance(accounts);
// 记录测试前的所有户头的余额
for (Account account : accounts) {
balanceTracking.put(account.getId(), account.getBalance());
}
// 生成所有测试线程
TestRunnable[] tr = new TestRunnable[numOfTransfers];
long start = System.currentTimeMillis();
for (int i = 0; i < tr.length; i++) {
tr[i] = generateTransferThread();
}
// 生成测试线程运行器
MultiThreadedTestRunner mttr = new MultiThreadedTestRunner(tr);
// 运行测试线程
mttr.runTestRunnables();
long used = System.currentTimeMillis() - start;
System.out.printf("Total: %s transfers used %s milli-seconds.\n", numOfTransfers, used);
// 获取测试后所有户头总余额
Account[] accounts2 = getAccounts();
BigDecimal total2 = accountService.getTotalBalance(accounts2);
// 确认测试前后,所有户头总余额还是一致的。
assertEquals(total1, total2);
// 确认测试前后,所有户头余额与转帐记录相一致。
System.out.printf("Successful transfers: %s\n", successTransfers);
System.out.println(balanceTracking);
for (Account account : accounts2) {
assertEquals(balanceTracking.get(account.getId()), account.getBalance());
}
}
/*
* 测试线程类定义
*/
protected static class TransferThread extends TestRunnable {
private AccountService accountService;
private Map<Long, BigDecimal> balanceTracking;
protected long[] accountIds;
protected long fromId;
protected long toId;
protected BigDecimal amount;
public TransferThread(AccountService accountService,
long[] accountIds, Map<Long, BigDecimal> balanceTracking) {
super();
this.accountService = accountService;
this.accountIds = accountIds;
this.balanceTracking = balanceTracking;
}
protected void generateTransferOptions() {
Random randomGenerator = new Random();
// 随机选取转出户头
fromId = accountIds[
randomGenerator.nextInt(accountIds.length)
];
// 随机选取转入户头
toId = accountIds[
randomGenerator.nextInt(accountIds.length)
];
// 确保转出、转入户头不是同一个
while (toId == fromId) {
toId = accountIds[
randomGenerator.nextInt(accountIds.length)
];
}
// 随机选取转帐数额(0 ~ 149元之间)
amount = BigDecimal.valueOf(randomGenerator.nextInt(150));
}
@Override
public void runTest() throws Throwable {
generateTransferOptions();
boolean success;
// 转帐!
try {
accountService.transfer(
accountService.findById(toId),
accountService.findById(fromId),
amount);
success = true;
} catch (AppException ae) {
// 捕捉运行时间异常“AppException”。在真实的系统中,这里必须通知用户:转帐失败,请稍后再试。
//System.out.println("AppException:" + ae.getMessage());
success = false;
} catch (Throwable t) {
// 捕捉所有异常。在真实的系统中,这里必须通知用户:转帐失败,请稍后再试。
success = false;
}
if (success) {
// 以下记录每一次成功的转帐后,被影响户头的余额。假如在 accountService.transfer 中有异常抛出,
// 这一记录动作将不会执行。
synchronized (balanceTracking) {
successTransfers ++;
BigDecimal oriFromBal = balanceTracking.get(fromId);
BigDecimal oriToBal = balanceTracking.get(toId);
System.out.printf("Successful transfer no.%s: account[%s] (bal: %s) -> account[%s] (bal: %s),"
+ " amount (%s)\n", successTransfers, fromId, oriFromBal, toId, oriToBal, amount);
balanceTracking.put(fromId, oriFromBal.subtract(amount));
balanceTracking.put(toId, oriToBal.add(amount));
}
}
}
}
}
分享到:
相关推荐
6. **测试与调试**:在"Spring事务小demo"项目中,可以编写JUnit测试类来验证事务的正确性。测试类中,使用@Test注解并配置@Test rollback属性,可以确保每次测试结束后,事务都会被回滚,保持数据库的原始状态。 ...
通常,测试类会模拟不同的事务场景,比如正常事务执行、事务回滚、并发事务等,确保事务管理的正确性和一致性。 总的来说,Spring的事务模板是编程式事务管理的一个强大工具,它使得在不依赖AOP的情况下处理事务变...
在Spring框架中,事务管理是一个重要的特性,允许开发者控制一系列操作的原子性,确保数据的一致性。Spring提供了声明式事务管理和编程式事务管理两种方式,其中声明式事务管理因其实现简单而被广泛应用。声明式事务...
在Spring框架中,事务管理是实现业务逻辑时不可或缺的一部分,它确保了数据的一致性和完整性。本篇文章将详细解析Spring中的事务类型,帮助你更好地理解和应用这些知识。 首先,Spring支持两种事务管理方式:编程式...
例如,通过Spring事务确保在一个事务内对多个对象的操作要么全部成功,要么全部失败,同时Hibernate的缓存减少对数据库的直接访问,提高系统性能。然而,合理配置缓存和事务策略以平衡性能和数据一致性是开发中的...
在Spring中,我们可以配置事务的传播行为,比如REQUIRED(默认,如果当前存在事务,则加入当前事务,否则新建一个事务)、PROPAGATION_SUPPORTS(如果当前存在事务,则加入,否则不开启事务)、PROPAGATION_REQUIRES...
这样,Spring可以在事务范围内正确地传播事务,即使在多线程环境下也能保证事务的正确性。 在Spring的`PlatformTransactionManager`接口中,`TransactionStatus`对象通常会用ThreadLocal来存储。当开始一个事务时,...
1. Spring提供了一个名为ThreadPoolTaskExecutor的实现,它基于Java的ExecutorService接口,允许我们自定义线程池配置,如核心线程数、最大线程数、队列容量、超时时间等。 2. 通过在配置文件中声明一个...
6. **测试**:使用JUnit进行单元测试,例如运行`JunitTestVillageArticle`的`modify`测试,验证事务的正确性。在测试类中,可以使用`@Transactional`注解开启一个新的事务,当测试结束后,如果发生异常,事务会被...
3. **隔离性 (Isolation)**:多个并发事务之间互不影响。每个事务在其执行过程中,应与其他事务隔离,以避免数据损坏。 4. **持久性 (Durability)**:一旦事务成功完成,其效果将是永久性的,即使之后系统出现故障也...
3. **隔离性(Isolation)**:多个并发事务之间的操作应该是独立的,即一个事务的操作不应干扰其他事务。这有助于避免数据冲突和不一致性问题。 4. **持久性(Durability)**:一旦事务被提交,其结果就是永久性的...
例如,一个事务可能涉及多个用户的资金转账,事务完成后,两个用户账户的总金额应该保持不变,这就是一致性。 3. **隔离性(Isolation)**:隔离性防止了事务之间的相互影响。数据库系统提供了多种事务隔离级别,包括...
在Spring框架中,事务管理不仅提供了ACID属性的支持,还引入了事务的传播特性,这些特性决定了当一个方法调用另一个方法时,事务如何进行交互。Spring提供了七种事务传播特性,每一种都有其特定的场景适用性。 1. *...
而PROPAGATION_REQUIRES_NEW则总是新建一个事务,即使在已有事务中调用,也会暂停当前事务,执行新的事务。 在实际开发中,我们还需要关注事务的隔离级别,包括READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ...
10. **事务回滚规则**:理解Spring的默认回滚规则和如何自定义回滚规则是保证事务正确性的关键。 通过以上内容,你应该已经对Spring Boot整合Spring事务有了全面的认识。在实际项目中,合理利用Spring的事务管理,...
声明式事务管理是Spring的一个强大特性,只需在服务层的方法上添加`@Transactional`注解,Spring就会自动处理事务的开始、提交、回滚等操作。 除了上述基本配置,你可能还需要关注以下几点: - 恢复机制:JTA支持...
在Java的Spring框架中,事务管理是至关重要的一个部分,特别是在多线程和并发环境下,保证数据的一致性和完整性。Spring提供了丰富的事务管理API来帮助开发者处理事务相关的操作。 首先,我们来看一下Spring事务...
Spring通过透明地管理Hibernate的Session,确保事务的正确性。这种模式被称为JPA(Java Persistence API)的“容器管理持久化”(CMT)。 2. **SessionFactory和Session:**SessionFactory是线程安全的,用于创建...
在`tx`和`aop`的配合下,Spring会通过AOP代理拦截带有`@Transactional`的方法调用,根据注解中的配置启动一个新的事务,执行方法,如果方法正常结束则提交事务,遇到异常则回滚事务。 在基于Struts1.2和Spring2.0的...