本文目录导读:

我来为您提供一个Java灰度发布(Gray Release) 的完整案例,包含多种常见实现方案。
灰度发布核心概念
灰度发布是指通过控制流量逐渐引入新版本,从少量用户开始测试,观察无误后再扩大范围,最终实现全量发布的过程。
完整实现案例
1 基础灰度路由方案(基于用户ID)
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 灰度发布控制器 - 基于用户ID的灰度路由
*/
@RestController
@RequestMapping("/api")
public class GrayReleaseController {
@Autowired
private GrayReleaseService grayReleaseService;
@Autowired
private V1Service v1Service;
@Autowired
private V2Service v2Service;
/**
* 灰度接口示例
*/
@GetMapping("/greet")
public String greet(@RequestHeader(value = "X-User-ID", required = false) String userId) {
// 判断是否命中灰度
if (grayReleaseService.isGrayUser(userId)) {
return v2Service.greetV2(userId);
} else {
return v1Service.greetV1(userId);
}
}
/**
* 更新灰度配置
*/
@PostMapping("/gray/config")
public String updateGrayConfig(@RequestBody GrayConfig config) {
grayReleaseService.updateConfig(config);
return "灰度配置更新成功";
}
}
2 灰度配置管理服务
import org.springframework.stereotype.Service;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class GrayReleaseService {
// 灰度配置存储(生产环境建议用Redis/配置中心)
private final Map<String, GrayConfig> grayConfigMap = new ConcurrentHashMap<>();
// 已灰度用户集合
private final Set<String> grayUsers = ConcurrentHashMap.newKeySet();
// 灰度流量计数器
private final AtomicInteger grayRequestCount = new AtomicInteger(0);
// Redis模板(可选)
@Autowired(required = false)
private RedisTemplate<String, Object> redisTemplate;
/**
* 初始化默认灰度配置
*/
@PostConstruct
public void init() {
// 默认灰度比例为10%
GrayConfig defaultConfig = new GrayConfig();
defaultConfig.setServiceName("user-service");
defaultConfig.setGrayRatio(10);
defaultConfig.setEnabled(true);
defaultConfig.setVersion("v2");
defaultConfig.setDescription("默认灰度配置");
grayConfigMap.put(defaultConfig.getServiceName(), defaultConfig);
// 预置一些灰度用户
grayUsers.addAll(Arrays.asList("1001", "1002", "1003", "1004", "1005"));
}
/**
* 判断用户是否为灰度用户
*/
public boolean isGrayUser(String userId) {
if (userId == null || userId.isEmpty()) {
return false;
}
// 方式1:基于用户ID白名单
if (grayUsers.contains(userId)) {
return true;
}
// 方式2:基于比例灰度
GrayConfig config = grayConfigMap.get("user-service");
if (config == null || !config.isEnabled()) {
return false;
}
// 根据用户ID哈希取模
int hash = Math.abs(userId.hashCode()) % 100;
return hash < config.getGrayRatio();
}
/**
* 基于流量比例灰度
*/
public boolean isGrayByRatio(int ratio) {
return grayRequestCount.incrementAndGet() % 100 < ratio;
}
/**
* 基于时间窗口灰度(例如每天9:00-18:00灰度)
*/
public boolean isGrayByTime() {
LocalTime now = LocalTime.now();
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(18, 0);
return !now.isBefore(start) && !now.isAfter(end);
}
/**
* 更新灰度配置
*/
public void updateConfig(GrayConfig newConfig) {
grayConfigMap.put(newConfig.getServiceName(), newConfig);
// 同步到Redis(可选)
if (redisTemplate != null) {
redisTemplate.opsForValue().set("gray:config:" + newConfig.getServiceName(), newConfig);
}
}
/**
* 添加灰度用户
*/
public void addGrayUser(String userId) {
grayUsers.add(userId);
}
/**
* 移除灰度用户
*/
public void removeGrayUser(String userId) {
grayUsers.remove(userId);
}
/**
* 获取当前灰度配置
*/
public GrayConfig getGrayConfig(String serviceName) {
GrayConfig config = grayConfigMap.get(serviceName);
if (config == null && redisTemplate != null) {
config = (GrayConfig) redisTemplate.opsForValue().get("gray:config:" + serviceName);
}
return config;
}
/**
* 灰度埋点 - 记录灰度用户操作
*/
public void trackGrayMetric(String userId, String requestPath) {
// 记录灰度指标,用于监控
System.out.println(String.format("[GRAY] User: %s, Path: %s, Time: %s",
userId, requestPath, new Date()));
}
}
3 灰度配置模型
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GrayConfig implements Serializable {
// 服务名称
private String serviceName;
// 版本号
private String version;
// 灰度比例(0-100)
private int grayRatio;
// 是否启用
private boolean enabled;
// 灰度规则描述
private String description;
// 创建时间
private Date createTime;
// 更新时间
private Date updateTime;
}
4 灰度过滤器(基于网关层)
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 灰度发布过滤器 - 基于HTTP请求头
*/
@Component
public class GrayFilter implements Filter {
// 灰度版本的服务地址
private static final Map<String, String> VERSION_URL_MAP = new ConcurrentHashMap<>();
static {
// 配置不同版本的服务地址
VERSION_URL_MAP.put("v1", "http://localhost:8081");
VERSION_URL_MAP.put("v2", "http://localhost:8082");
}
// 白名单用户
private static final Set<String> WHITE_LIST = ConcurrentHashMap.newKeySet();
static {
WHITE_LIST.add("1001");
WHITE_LIST.add("1002");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 获取用户ID
String userId = httpRequest.getHeader("X-User-ID");
// 判断是否为灰度请求
String version = "v1"; // 默认版本
if (userId != null && (WHITE_LIST.contains(userId) || isGrayByRatio(userId))) {
version = "v2"; // 灰度版本
httpResponse.setHeader("X-Gray-Version", "v2");
}
// 可以通过Header传递给下游服务
httpRequest.setAttribute("grayVersion", version);
// 继续链式调用
chain.doFilter(request, response);
}
}
5 基于Spring Cloud Gateway的灰度路由
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Mono;
/**
* 基于Spring Cloud Gateway的灰度路由
*/
@Configuration
public class GrayRouteConfiguration {
/**
* 配置灰度路由规则
*/
@Bean
public RouteLocator grayRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service-v1", r -> r
.path("/api/user/**")
.and()
.header("X-Version", "v1")
.uri("http://user-v1-service:8081"))
.route("user-service-v2", r -> r
.path("/api/user/**")
.and()
.header("X-Version", "v2")
.uri("http://user-v2-service:8082"))
.build();
}
/**
* 自定义灰度全局过滤器
*/
@Bean
public GlobalFilter grayGlobalFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 获取用户信息
String userId = request.getHeaders().getFirst("X-User-ID");
// 判断是否灰度用户
if (userId != null && GrayRuleService.isGrayUser(userId)) {
ServerHttpRequest newRequest = request.mutate()
.header("X-Version", "v2")
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
}
// 默认走v1版本
ServerHttpRequest newRequest = request.mutate()
.header("X-Version", "v1")
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
};
}
}
6 灰度监控与统计
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.atomic.AtomicLong;
import java.time.LocalDate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 灰度监控统计服务
*/
@Service
public class GrayMonitorService {
// 统计各版本请求量
private final Map<String, AtomicLong> requestCount = new ConcurrentHashMap<>();
// 统计各版本错误量
private final Map<String, AtomicLong> errorCount = new ConcurrentHashMap<>();
// 记录灰度用户操作
private final Map<String, List<GrayLog>> grayLogs = new ConcurrentHashMap<>();
/**
* 记录请求
*/
public void recordRequest(String version, String userId, boolean success) {
// 增加请求计数
requestCount.computeIfAbsent(version, k -> new AtomicLong()).incrementAndGet();
// 记录错误
if (!success) {
errorCount.computeIfAbsent(version, k -> new AtomicLong()).incrementAndGet();
}
// 记录灰度用户日志
if ("v2".equals(version)) {
GrayLog log = new GrayLog(userId, userId, success, "gray-request");
grayLogs.computeIfAbsent(userId, k -> new ArrayList<>()).add(log);
// 检查是否存在严重错误
checkGrayHealth(version);
}
}
/**
* 检查灰度健康状态
*/
private void checkGrayHealth(String version) {
long total = requestCount.getOrDefault(version, new AtomicLong()).get();
long errors = errorCount.getOrDefault(version, new AtomicLong()).get();
// 如果错误率超过20%,自动回滚
if (total >= 100 && errors > 0 && (errors * 100.0 / total) > 20) {
// 触发自动回滚
rollback();
}
}
/**
* 自动回滚
*/
private void rollback() {
// 恢复v1版本,停止灰度
GrayReleaseController controller = new GrayReleaseController();
// 实际项目中通过配置中心动态切换
System.err.println("[GRAY] 灰度异常,正在自动回滚...");
}
/**
* 获取灰度统计报表
*/
public GrayReport getGrayReport() {
return GrayReport.builder()
.date(LocalDate.now().toString())
.v1RequestCount(requestCount.getOrDefault("v1", new AtomicLong()).get())
.v2RequestCount(requestCount.getOrDefault("v2", new AtomicLong()).get())
.v1ErrorCount(errorCount.getOrDefault("v1", new AtomicLong()).get())
.v2ErrorCount(errorCount.getOrDefault("v2", new AtomicLong()).get())
.build();
}
}
7 服务版本实现
import org.springframework.stereotype.Service;
/**
* V1版本服务
*/
@Service
public class V1Service {
public String greetV1(String userId) {
return String.format("你好 %s,这是V1版本服务", userId);
}
public User getUserV1(Long id) {
return new User(id, "v1-user-" + id);
}
}
/**
* V2版本服务(灰度服务)
*/
@Service
public class V2Service {
public String greetV2(String userId) {
return String.format("您好 %s,这是V2版本服务(含新功能)", userId);
}
public User getUserV2(Long id) {
return new User(id, "v2-user-" + id, "VIP用户");
}
}
8 Kubernetes环境灰度配置示例
# 灰度部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service-v1
spec:
replicas: 3
selector:
matchLabels:
app: user-service
version: v1
template:
metadata:
labels:
app: user-service
version: v1
spec:
containers:
- name: user-service
image: user-service:v1.0.0
ports:
- containerPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service-v2
spec:
replicas: 1 # 先部署1个实例
selector:
matchLabels:
app: user-service
version: v2
template:
metadata:
labels:
app: user-service
version: v2
spec:
containers:
- name: user-service
image: user-service:v2.0.0
ports:
- containerPort: 8080
---
# 服务入口
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
---
# Istio虚拟机灰度配置
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- user-service
http:
- match:
- headers:
X-Gray:
exact: "true"
route:
- destination:
host: user-service
subset: v2
weight: 100
- route:
- destination:
host: user-service
subset: v1
weight: 100
使用方式
1 测试接口
# 正常版本
curl -X GET http://localhost:8080/api/greet -H "X-User-ID: 2000"
# 灰度版本(命中白名单)
curl -X GET http://localhost:8080/api/greet -H "X-User-ID: 1001"
# 更新灰度配置
curl -X POST http://localhost:8080/api/gray/config \
-H "Content-Type: application/json" \
-d '{
"serviceName": "user-service",
"version": "v2",
"grayRatio": 30,
"enabled": true
}'
2 灰度策略汇总
| 策略类型 | 适用场景 | 实现方式 |
|---|---|---|
| 用户白名单 | 内测、指定用户 | Set集合匹配 |
| 比例灰度 | 逐步放量 | 哈希取模 |
| 基于Header | A/B测试 | HTTP头匹配 |
| 基于时间 | 业务低峰期 | 时间窗口 |
| 动态配置 | 实时调整 | Redis/配置中心 |
| 自动回滚 | 异常降级 | 错误率监控 |
最佳实践建议
- 灰度范围控制:建议从1%开始,逐步增加到5%,10%,20%,50%,100%
- 监控报警:实时监控灰度版本的错误率、延迟、用户反馈
- 自动回滚机制:设置错误率阈值(如5%),超限自动回滚
- 灰度日志:记录完整的灰度用户操作日志,便于问题追踪
- 数据一致性:做好灰度期间的数据迁移和兼容
- 配置分离:灰度开关独立管理,灵活开关
这个案例覆盖了Java灰度发布的核心场景,您可以根据实际需求进行调整和扩展。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播