How to Efficiently Manage Test User Login Credentials with Thread‑Safe Caching in Java
This article explains a practical approach to validate test‑account login status by using a special syntax, per‑user locking, and thread‑safe caching to avoid repeated logins, while discussing transaction isolation levels and propagation settings needed for reliable backend testing.
The author faced the problem of validating test‑account login credentials, which expire and can be evicted, making it cumbersome to maintain login state for all test users and wasteful for server performance.
Two scenarios were proposed:
Scenario A : During single‑test debugging, reserve a validity period for a credential. After expiration, re‑validate; if still valid, reset the period, otherwise obtain a fresh credential via the login API.
Scenario B : When running a suite of tests concurrently, allow only one thread per user to execute the logic from Scenario A, caching the credential in a temporary Map so that repeated database reads are avoided.
To ensure all threads see the latest credential and that reads from the cache are correct, the author introduced a per‑user lock inside the JVM. Each user ID maps to a lock object; only one thread can read or write that user's credential at a time, preventing redundant logins during rapid test execution. After a test suite finishes, the temporary map is released and reclaimed by the garbage collector.
User Lock Storage Class
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<>();
/**
* Get lock object for a test user
* @param id user id
* @return lock object
*/
public static Object get(int id) {
certificates.compute(id, (key, value) -> {
if (value == null) {
value = new Object();
}
return value;
});
return certificates.get(id);
}
}Business Implementation
@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();
}
}The author discovered that relying solely on transaction isolation and propagation to achieve the same effect introduced bugs because updating a user's login credential involves multiple reads and writes; multithreaded handling caused inconsistencies.
Transaction Isolation Levels
DEFAULT – uses the database's default, typically READ_COMMITTED.
READ_UNCOMMITTED – allows dirty reads; rarely used.
READ_COMMITTED – prevents dirty reads; recommended for most cases.
REPEATABLE_READ – prevents dirty and non‑repeatable reads; suitable for many scenarios.
SERIALIZABLE – fully isolates transactions but severely impacts performance.
Transaction Propagation Behaviors
REQUIRED – join existing transaction or create a new one.
SUPPORTS – join if present, otherwise run non‑transactionally.
MANDATORY – must run within a transaction, else throw.
REQUIRES_NEW – always start a new transaction, suspending any existing one.
NOT_SUPPORTED – run non‑transactionally, suspending any existing transaction.
NEVER – must not run within a transaction, else throw.
NESTED – creates a nested transaction if a transaction exists; otherwise behaves like REQUIRED.
By combining per‑user locking, temporary caching, and careful transaction configuration, the solution ensures that each test user logs in only once per short period, reduces redundant database calls, and maintains thread‑safe access to credentials during parallel test execution.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
