本文目录导读:

这是一个相对复杂的技术话题,我来为你系统介绍Java服务回放(Service Replay)的实现方案。
什么是服务回放
服务回放是指记录线上服务的真实请求和响应,在需要时重放这些请求,用于测试、验证或问题复现。
核心实现方案
基于代理的模式(推荐)
// 请求记录器接口
public interface RequestRecorder {
void record(RequestEntry entry);
}
// 请求回放器接口
public interface RequestReplayer {
ResponseEntry replay(String requestId);
}
使用AOP切面
@Aspect
@Component
public class ServiceReplayAspect {
@Autowired
private RequestRecorder recorder;
@Around("@annotation(ServiceReplay)")
public Object aroundService(ProceedingJoinPoint joinPoint) throws Throwable {
// 生成请求ID
String requestId = UUID.randomUUID().toString();
// 记录请求参数和时间
Object[] args = joinPoint.getArgs();
long startTime = System.currentTimeMillis();
try {
// 执行原始方法
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
// 记录成功请求
recorder.record(new RequestEntry(
requestId,
joinPoint.getTarget().getClass().getName(),
joinPoint.getSignature().getName(),
args,
result,
true,
duration
));
return result;
} catch (Throwable throwable) {
// 记录失败请求
recorder.record(new RequestEntry(
requestId,
joinPoint.getTarget().getClass().getName(),
joinPoint.getSignature().getName(),
args,
throwable,
false,
System.currentTimeMillis() - startTime
));
throw throwable;
}
}
}
// 注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceReplay {
boolean recordOnly() default false; // true表示只记录不回放
}
// 请求记录数据结构
@Data
@AllArgsConstructor
public class RequestEntry implements Serializable {
private String requestId;
private String className;
private String methodName;
private Object[] args;
private Object result;
private boolean success;
private long duration;
private long timestamp = System.currentTimeMillis();
}
基于存储的完整实现
@Service
public class ReplayService {
@Autowired
private RedisTemplate<String, RequestEntry> redisTemplate;
@Autowired
private ApplicationContext applicationContext;
// 配置 - 可以根据环境动态调整
@Value("${replay.enabled:true}")
private boolean replayEnabled;
@Value("${replay.storage.type:redis}")
private String storageType;
/**
* 记录请求
*/
public void recordRequest(RequestEntry entry) {
if (!replayEnabled) return;
String key = buildRedisKey(entry.getClassName(), entry.getMethodName());
// 使用Redis List存储请求记录
// 设置过期时间(例如7天)
redisTemplate.opsForList().leftPush(key, entry);
redisTemplate.expire(key, 7, TimeUnit.DAYS);
// 限制单个API的请求记录数量(防止内存溢出)
Long size = redisTemplate.opsForList().size(key);
if (size != null && size > 10000) {
redisTemplate.opsForList().rightPop(key);
}
}
/**
* 回放指定API的历史请求
*/
public List<ReplayResult> replayRequests(String className, String methodName,
int count, Long startTime, Long endTime) {
String key = buildRedisKey(className, methodName);
List<RequestEntry> entries = redisTemplate.opsForList().range(key, 0, count - 1);
List<ReplayResult> results = new ArrayList<>();
// 过滤时间范围
if (entries != null) {
entries.stream()
.filter(e -> e.getTimestamp() >= (startTime != null ? startTime : 0))
.filter(e -> e.getTimestamp() <= (endTime != null ? endTime : Long.MAX_VALUE))
.limit(count)
.forEach(entry -> {
ReplayResult result = replaySingleRequest(entry);
results.add(result);
});
}
return results;
}
/**
* 回放单个请求
*/
private ReplayResult replaySingleRequest(RequestEntry entry) {
ReplayResult result = new ReplayResult();
result.setRequestId(entry.getRequestId());
result.setOriginalResult(entry.getResult());
try {
// 获取bean
Object bean = applicationContext.getBean(Class.forName(entry.getClassName()));
Method method = bean.getClass().getMethod(entry.getMethodName(),
getParameterTypes(entry.getArgs()));
// 记录回放开始时间
long startTime = System.currentTimeMillis();
// 执行请求
Object replayResult = method.invoke(bean, entry.getArgs());
// 比较结果
result.setReplayResult(replayResult);
result.setDuration(System.currentTimeMillis() - startTime);
result.setSuccess(true);
result.setMatch(compareResults(entry.getResult(), replayResult));
} catch (Exception e) {
result.setSuccess(false);
result.setErrorMessage(e.getMessage());
}
return result;
}
// 构建Redis Key
private String buildRedisKey(String className, String methodName) {
return "replay:" + className + ":" + methodName;
}
// 获取参数类型
private Class<?>[] getParameterTypes(Object[] args) {
if (args == null) return new Class[0];
return Arrays.stream(args)
.map(arg -> arg != null ? arg.getClass() : Object.class)
.toArray(Class[]::new);
}
// 结果比较
private boolean compareResults(Object original, Object replay) {
if (original == null && replay == null) return true;
if (original == null || replay == null) return false;
return original.equals(replay);
}
}
// 回放结果
@Data
public class ReplayResult {
private String requestId;
private Object originalResult;
private Object replayResult;
private boolean success;
private boolean match;
private long duration;
private String errorMessage;
}
高级实现:支持并发和分布式
@Component
public class DistributedReplayManager {
@Autowired
private RedissonClient redissonClient;
@Autowired
private KafkaTemplate<String, RequestEntry> kafkaTemplate;
private static final String LOCK_KEY_PREFIX = "replay:lock:";
/**
* 使用Kafka实现去耦合记录
*/
public void recordViaKafka(RequestEntry entry) {
// 发送到Kafka topic
kafkaTemplate.send("replay-requests", entry.getRequestId(), entry);
}
/**
* 分布式回放(带锁控制)
*/
public void distributedReplay(ReplayConfig config, String nodeId) {
// 获取分布式锁,防止多节点同时回放
String lockKey = LOCK_KEY_PREFIX + config.getApiKey();
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 检查回放是否已在进行
if (isReplaying(config.getApiKey(), nodeId)) {
return;
}
// 标记本节点开始回放
markReplaying(config.getApiKey(), nodeId, true);
// 执行回放
performReplay(config);
// 标记回放完成
markReplaying(config.getApiKey(), nodeId, false);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private boolean isReplaying(String apiKey, String nodeId) {
// 检查Redis中回放状态
String statusKey = "replay:status:" + apiKey;
String currentReplayer = redisTemplate.opsForValue().get(statusKey);
return currentReplayer != null && !currentReplayer.equals(nodeId);
}
private void markReplaying(String apiKey, String nodeId, boolean isPlaying) {
String statusKey = "replay:status:" + apiKey;
if (isPlaying) {
redisTemplate.opsForValue().set(statusKey, nodeId, 1, TimeUnit.HOURS);
} else {
redisTemplate.delete(statusKey);
}
}
}
// 回放配置
@Data
public class ReplayConfig {
private String apiKey; // API标识
private int replayCount; // 回放次数
private int concurrencyLevel; // 并发级别
private boolean recordOnly; // 仅记录模式
private Long startTime; // 起始时间
private Long endTime; // 结束时间
}
使用场景示例
@RestController
@RequestMapping("/replay")
public class ReplayController {
@Autowired
private ReplayService replayService;
@Autowired
private DistributedReplayManager distributedReplayManager;
/**
* 手动触发回放
*/
@PostMapping("/execute")
public ApiResponse executeReplay(@RequestBody ReplayRequest request) {
List<ReplayResult> results = replayService.replayRequests(
request.getClassName(),
request.getMethodName(),
request.getCount() != null ? request.getCount() : 100,
request.getStartTime(),
request.getEndTime()
);
// 统计回放结果
long successCount = results.stream().filter(ReplayResult::isSuccess).count();
long matchCount = results.stream().filter(ReplayResult::isMatch).count();
return ApiResponse.success(new ReplayStatistics(
results.size(),
successCount,
matchCount,
results
));
}
/**
* 查看回放历史
*/
@GetMapping("/history")
public ApiResponse getHistory(String apiKey, @RequestParam(defaultValue = "10") int pageSize) {
// 查询回放记录
return ApiResponse.success(replayHistoryService.getHistory(apiKey, pageSize));
}
}
// 在业务服务中使用
@Service
public class UserService {
@ServiceReplay // 标记需要回放的接口
public UserResponse queryUser(UserQuery query) {
// 业务逻辑
return userRepository.findByQuery(query);
}
@ServiceReplay(recordOnly = true) // 只记录,不参与回放
public void sendNotification(Notification notification) {
// 发送通知(不纳入回放范围)
}
}
最佳实践
性能考虑
# application.yml
replay:
enabled: true
storage:
type: redis
max-count-per-api: 10000
expire-days: 7
async: true # 使用异步记录,不影响主流程
concurrency: 8 # 回放并发数
filter:
ignore-params: password, secret # 忽略敏感参数
安全性
public class ReplaySecurityManager {
// 参数脱敏
public RequestEntry sanitizeRequest(RequestEntry entry) {
if (entry.getArgs() != null) {
for (int i = 0; i < entry.getArgs().length; i++) {
entry.getArgs()[i] = desensitize(entry.getArgs()[i]);
}
}
return entry;
}
private Object desensitize(Object param) {
// 对敏感字段进行脱敏处理
if (param instanceof String && isSensitive((String) param)) {
return "******";
}
return param;
}
private boolean isSensitive(String param) {
// 检查是否为敏感字段(密码、手机号等)
return param.matches("^1[3-9]\\d{9}$") || param.length() > 8;
}
}
回放策略选择
public enum ReplayStrategy {
/**
* 精确回放 - 完全按历史顺序
*/
EXACT,
/**
* 压力回放 - 调整并发和频率
*/
STRESS,
/**
* 采样回放 - 只回放特定比例
*/
SAMPLING,
/**
* 异常回放 - 只回放之前失败的请求
*/
ERROR_ONLY
}
注意事项
- 幂等性:确保回放的接口是幂等的
- 环境隔离:不要在线上环境直接回放
- 数据一致性:注意时间相关数据的处理
- 监控告警:对回放过程中的异常进行监控
- 存储成本:合理设置存储周期和大小
这个方案可以很好地支持服务测试、问题复现和性能压测等场景,根据具体需求,你可以选择合适的实现方式。