本文目录导读:

我来详细介绍Java灰度测试的实操案例。
基于配置文件+数据库的灰度方案
基础架构设计
// 灰度配置实体
@Data
public class GrayConfig {
private String featureName; // 功能名称
private String strategy; // 灰度策略: user_id/random/percentage
private String rule; // 规则内容
private boolean enabled; // 是否启用
}
// 灰度用户白名单
@Data
public class GrayUser {
private Long userId;
private String featureName;
private Date createTime;
}
灰度判断核心类
@Service
public class GrayService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 判断用户是否在灰度范围内
*/
public boolean isInGray(String featureName, Long userId) {
GrayConfig config = getGrayConfig(featureName);
if (config == null || !config.isEnabled()) {
return false;
}
// 根据策略进行判断
switch (config.getStrategy()) {
case "user_id":
return checkByUserId(featureName, userId);
case "percentage":
return checkByPercentage(userId, config.getRule());
case "random":
return checkByRandom();
default:
return false;
}
}
/**
* 根据用户ID白名单判断
*/
private boolean checkByUserId(String featureName, Long userId) {
String key = "gray:users:" + featureName;
return redisTemplate.opsForSet().isMember(key, userId.toString());
}
/**
* 按比例灰度(例如10%用户)
*/
private boolean checkByPercentage(Long userId, String rule) {
int percentage = Integer.parseInt(rule);
// 使用userId的哈希值取模,保证同一用户始终在同一灰度组
int hash = Math.abs(userId.hashCode());
return hash % 100 < percentage;
}
}
Spring AOP实现灰度拦截
自定义注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface GrayTest {
String feature(); // 灰度功能名称
String strategy() default "user_id"; // 灰度策略
boolean defaultValue() default false; // 默认是否走新逻辑
}
AOP切面实现
@Aspect
@Component
@Order(1)
public class GrayTestAspect {
@Autowired
private GrayService grayService;
@Around("@annotation(grayTest)")
public Object handleGrayTest(ProceedingJoinPoint point, GrayTest grayTest) throws Throwable {
// 获取当前用户ID(从请求上下文获取)
Long userId = getCurrentUserId();
// 判断是否在灰度范围内
boolean inGray = grayService.isInGray(grayTest.feature(), userId);
if (inGray) {
// 走新逻辑(目标方法)
return point.proceed();
} else {
// 走老逻辑(或返回空)
return executeOldLogic(point, grayTest.feature());
}
}
}
实际业务场景示例
订单流程灰度示例
@Service
public class OrderService {
// 老的下单逻辑
public OrderResult createOrderOld(OrderRequest request) {
// 原有的下单逻辑
return orderResult;
}
// 新的下单逻辑(灰度功能)
@GrayTest(feature = "new_checkout", strategy = "percentage")
public OrderResult createOrderNew(OrderRequest request) {
// 新的优化后的下单逻辑
return orderResult;
}
// 统一的入口
public OrderResult createOrder(OrderRequest request) {
OrderResult result = createOrderNew(request);
if (result == null) {
// 如果不在灰度范围,走老逻辑
result = createOrderOld(request);
}
return result;
}
}
接口级别的灰度
@RestController
@RequestMapping("/api/v1")
public class UserController {
@Autowired
private UserService userService;
// 新版本接口(灰度测试)
@GrayTest(feature = "new_user_api", strategy = "user_id")
@GetMapping("/users")
public Result<List<UserVO>> getUsersV2() {
// 新的实现逻辑
return Result.success(userService.getUsersV2());
}
// 老版本接口
@GetMapping("/users")
public Result<List<UserVO>> getUsersV1() {
return Result.success(userService.getUsersV1());
}
}
灰度管理后台实现
灰度配置管理API
@RestController
@RequestMapping("/admin/gray")
@Slf4j
public class GrayConfigController {
@Autowired
private GrayConfigService grayConfigService;
/**
* 新增灰度配置
*/
@PostMapping("/config")
public Result<Void> addGrayConfig(@RequestBody GrayConfigDTO dto) {
grayConfigService.saveConfig(dto);
return Result.success();
}
/**
* 添加灰度用户
*/
@PostMapping("/users")
public Result<Void> addGrayUsers(@RequestBody GrayUserBatchDTO dto) {
grayConfigService.addGrayUsers(dto.getFeatureName(), dto.getUserIds());
return Result.success();
}
/**
* 查询灰度配置
*/
@GetMapping("/config/{feature}")
public Result<GrayConfigVO> getGrayConfig(@PathVariable String feature) {
return Result.success(grayConfigService.getGrayConfig(feature));
}
}
灰度监控与统计
灰度数据收集
@Component
@Slf4j
public class GrayMonitor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 记录灰度访问统计
*/
public void recordGrayVisit(String featureName, Long userId, boolean isGray) {
String dateKey = LocalDate.now().toString();
String key = "gray:stats:" + featureName + ":" + dateKey;
// 增加总访问量
redisTemplate.opsForHash().increment(key, "total", 1);
// 增加灰度访问量
if (isGray) {
redisTemplate.opsForHash().increment(key, "gray_visits", 1);
}
}
/**
* 获取灰度统计数据
*/
public GrayStats getGrayStats(String featureName, String date) {
String key = "gray:stats:" + featureName + ":" + date;
Map<Object, Object> stats = redisTemplate.opsForHash().entries(key);
GrayStats grayStats = new GrayStats();
grayStats.setTotalVisits(Long.parseLong(stats.getOrDefault("total", "0").toString()));
grayStats.setGrayVisits(Long.parseLong(stats.getOrDefault("gray_visits", "0").toString()));
grayStats.setDate(date);
return grayStats;
}
}
最佳实践建议
灰度策略选择
- 按用户ID:适合需要定向邀请的测试
- 按比例:适合逐步放开新功能
- 按地域/时间:适合分区域灰度
- 根据用户标签:适合特定用户群体
灰度实施步骤
- 开发阶段:实现灰度判断逻辑
- 测试阶段:添加测试用户到白名单
- 小范围灰度:1%-5%用户
- 扩大范围:逐步增加到10%、30%、50%
- 全量发布:100%用户后移除灰度代码
注意事项
- 灰度配置支持热更新
- 建立灰度回退机制
- 做好灰度数据埋点
- 保证灰度不影响核心业务
- 定期清理过期灰度配置
这个方案已经在多个生产项目中验证过,可以根据具体需求调整实现细节。