「原创声明:保留所有权利,禁止转载」
今天主要解决了一下测试账号登录状态的校验,我现在的方案是用户在写测试用例的时候使用特殊语法uid=123
这样的形式,表示该用例字段应该去uid
等于123
的测试账号的登录凭证。难点在于登录凭证会过期,会被挤掉,如果维护所有测试用户的登录状态又很麻烦,对服务器性能也是一种浪费。
所以最终的方案如下:
- A、单独用例调试过程中使用中,预留一个凭证的有效期。有效期过了之后会继续校验凭证的有效性,如果成功,则重置有效期,如果失败则从登陆接口重新获取用户登录凭证并更新有效期
- B、运行用例集(多个用例)时,采用多线程并发执行,为了保证每个测试用户的登录凭证有效性,每次只允许一个线程去执行 A 的逻辑。运行用例集过程中对用户凭证进行缓存,这样就不用从数据库中重复读取了。
在并行运行测试用例的时候,如果保证所以线程都能读到最新的用户凭证,且往缓存map
中读取的数据正确性,想了一个方案就是在JVM
里面对每一个用户进行加锁的操作,保证每一次只有一个线程去读写用户登录凭证。这样在单个用户运行的时候就可以登录一次,短时间内不用去登录即可持续进行用例的调试。在运行用例集的时候,每次运行创建一个临时的map
存放本次用到的用户登录凭证,用例集内的测试用例执行完,该对象就释放,等着被GC
回收。
用户对象锁存放类
- 这里用到之前一个知识点:Lambda 表达式在线程安全 Map 中应用。
package com.okay.family.common.basedata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
public class UserCertificate {
private static Logger logger = LoggerFactory.getLogger(UserCertificate.class);
private static ConcurrentHashMap<Integer, Object> certificates = new ConcurrentHashMap<>();
/**
* 获取锁对象,用户测试用户锁
*
* @param id
* @return
*/
public static Object get(int id) {
certificates.compute(id, (key, value) ->
{
if (value == null) {
value = new Object();
}
return value;
});
return certificates.get(id);
}
}
业务实现类
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRES_NEW)
public TestUserCheckBean getCertificate(int id) {
Object o = UserCertificate.get(id);
synchronized (o) {
TestUserCheckBean user = testUserMapper.findUser(id);
String create_time = user.getCreate_time();
long create = Time.getTimestamp(create_time);
long now = Time.getTimeStamp();
if (now - create < OkayConstant.CERTIFICATE_TIMEOUT && user.getStatus() == UserState.OK.getCode())
return user;
boolean b = UserUtil.checkUserLoginStatus(user);
if (!b) {
UserUtil.updateUserStatus(user);
}
testUserMapper.updateUserStatus(user);
return user;
}
}
@Override
public String getCertificate(int id, ConcurrentHashMap<Integer, String> map) {
Object o = UserCertificate.get(id);
synchronized (o) {
if (map.contains(id)) return map.get(id);
TestUserCheckBean user = testUserMapper.findUser(id);
String create_time = user.getCreate_time();
long create = Time.getTimestamp(create_time);
long now = Time.getTimeStamp();
if (now - create < OkayConstant.CERTIFICATE_TIMEOUT && user.getStatus() == UserState.OK.getCode()) {
map.put(id, user.getCertificate());
return user.getCertificate();
}
boolean b = UserUtil.checkUserLoginStatus(user);
if (!b) {
UserUtil.updateUserStatus(user);
if (user.getStatus()!=UserState.OK.getCode()) UserStatusException.fail();
}
map.put(id, user.getCertificate());
testUserMapper.updateUserStatus(user);
return user.getCertificate();
}
}
- 这里用到了事务隔离级别和事务传播行为的设置,因为本来想用这种方式达到效果,但后来发现不行,因为更新用户登录凭证涉及到好几次读写数据库,多线程处理肯定会出BUG。
附上事务隔离级别和传播行为的文档
事务隔离级别
隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。
- DEFAULT :这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是: READ_COMMITTED 。
- READ_UNCOMMITTED :该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
- READ_COMMITTED :该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
- REPEATABLE_READ :该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
- SERIALIZABLE :所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
传播行为
所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。
- REQUIRED :如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- SUPPORTS :如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- MANDATORY :如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- REQUIRES_NEW :创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- NOT_SUPPORTED :以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- NEVER :以非事务方式运行,如果当前存在事务,则抛出异常。
- NESTED :如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED 。
-
郑重声明:公众号 “FunTester” 首发,欢迎关注,禁止第三方转载。更多原创文章:FunTester 十八张原创专辑,合作请联系
Fhaohaizi@163.com
。
TesterHome 为用户提供「保留所有权利,禁止转载」的选项。
除非获得原作者的单独授权,任何第三方不得转载标注了「原创声明:保留所有权利,禁止转载」的内容,否则均视为侵权。
具体请参见TesterHome 知识产权保护协议。
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。