SpringBoot + 规则灰度发布 + 百分比流量切分:新规则先对 1% 用户生效,验证无误再全量
导语
在企业应用中,规则变更往往涉及业务逻辑的调整,直接全量发布可能带来较大的风险。灰度发布是一种有效的风险控制策略,通过将新规则先对小部分用户生效,验证无误后再逐步扩大范围,最终实现全量发布。
一、灰度发布的概念与原理
1.1 什么是灰度发布
灰度发布(Gray Release)是一种软件发布策略,通过将新功能先对一部分用户开放,验证无误后再逐步扩大范围,最终实现全量发布。在规则系统中,灰度发布可以用于验证新规则的效果,确保规则变更不会对业务造成负面影响。
1.2 灰度发布的优势
| 优势 | 描述 |
|---|---|
| 风险控制 | 小范围验证,降低发布风险 |
| 快速回滚 | 出现问题时可以快速回滚 |
| 用户反馈 | 收集用户反馈,优化规则 |
| 性能验证 | 验证新规则的性能影响 |
| 平滑过渡 | 实现规则的平滑过渡 |
1.3 灰度发布的策略
1. 基于用户的灰度
- 按用户 ID 或用户属性进行灰度
- 适用于需要用户体验反馈的场景
2. 基于流量的灰度
- 按请求比例进行灰度
- 适用于性能验证和稳定性测试
3. 基于时间的灰度
- 按时间逐步扩大灰度范围
- 适用于计划中的发布
4. 基于地域的灰度
- 按地域进行灰度
- 适用于区域性业务
二、技术方案设计
2.1 架构设计
flowchart TD
subgraph 接入层
A[客户端请求] -->|HTTP| B[SpringBoot 应用]
end
subgraph 路由层
B --> C[规则路由器]
C -->|灰度流量| D[新规则引擎]
C -->|正常流量| E[旧规则引擎]
end
subgraph 服务层
D --> F[规则执行服务]
E --> F
G[灰度配置服务] --> C
H[规则管理服务] --> D
H --> E
end
subgraph 存储层
I[规则数据库] --> H
J[灰度配置数据库] --> G
end
2.2 核心组件
- 规则路由器:根据灰度配置将请求路由到新规则或旧规则
- 灰度配置服务:管理灰度发布的配置,如灰度比例、灰度用户等
- 规则管理服务:管理规则的版本和生命周期
- 规则执行服务:执行规则并返回结果
- 监控服务:监控规则执行情况和灰度效果
2.3 技术选型
| 技术 | 版本 | 用途 |
|---|---|---|
| SpringBoot | 2.7.14 | 应用框架 |
| Spring Data JPA | - | 数据访问 |
| Redis | 7.0+ | 缓存和分布式锁 |
| H2 Database | - | 嵌入式数据库 |
| Spring Cloud Sleuth | - | 分布式追踪 |
| Micrometer | 1.10.0 | 指标收集 |
| Thymeleaf | - | 模板引擎 |
三、核心实现
3.1 依赖配置
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Cloud Sleuth -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- Micrometer -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2 数据模型
Rule.java
@Entity
@Table(name = "rule")
@Data
@NoArgsConstructor
public class Rule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "rule_name", nullable = false)
private String ruleName;
@Column(name = "rule_code", nullable = false)
private String ruleCode;
@Column(name = "rule_content", columnDefinition = "text", nullable = false)
private String ruleContent;
@Column(name = "version", nullable = false)
private Integer version;
@Column(name = "status", nullable = false)
private String status; // ACTIVE, INACTIVE, GRAY
@Column(name = "created_time", nullable = false)
private LocalDateTime createdTime;
@Column(name = "updated_time")
private LocalDateTime updatedTime;
@PrePersist
protected void onCreate() {
createdTime = LocalDateTime.now();
updatedTime = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedTime = LocalDateTime.now();
}
}
GrayReleaseConfig.java
@Entity
@Table(name = "gray_release_config")
@Data
@NoArgsConstructor
public class GrayReleaseConfig {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "rule_id", nullable = false)
private Long ruleId;
@Column(name = "rule_code", nullable = false)
private String ruleCode;
@Column(name = "gray_version", nullable = false)
private Integer grayVersion;
@Column(name = "base_version", nullable = false)
private Integer baseVersion;
@Column(name = "gray_percentage", nullable = false)
private Integer grayPercentage; // 0-100
@Column(name = "status", nullable = false)
private String status; // ACTIVE, PAUSED, COMPLETED
@Column(name = "start_time")
private LocalDateTime startTime;
@Column(name = "end_time")
private LocalDateTime endTime;
@Column(name = "created_time", nullable = false)
private LocalDateTime createdTime;
@Column(name = "updated_time")
private LocalDateTime updatedTime;
@PrePersist
protected void onCreate() {
createdTime = LocalDateTime.now();
updatedTime = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedTime = LocalDateTime.now();
}
}
RuleExecutionLog.java
@Entity
@Table(name = "rule_execution_log")
@Data
@NoArgsConstructor
public class RuleExecutionLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "rule_code", nullable = false)
private String ruleCode;
@Column(name = "version", nullable = false)
private Integer version;
@Column(name = "is_gray", nullable = false)
private Boolean isGray;
@Column(name = "user_id")
private String userId;
@Column(name = "request_data", columnDefinition = "text")
private String requestData;
@Column(name = "response_data", columnDefinition = "text")
private String responseData;
@Column(name = "execution_time", nullable = false)
private Long executionTime;
@Column(name = "success", nullable = false)
private Boolean success;
@Column(name = "error_message")
private String errorMessage;
@Column(name = "created_time", nullable = false)
private LocalDateTime createdTime;
@PrePersist
protected void onCreate() {
createdTime = LocalDateTime.now();
}
}
3.3 灰度配置服务
GrayReleaseService.java
@Service
@Slf4j
public class GrayReleaseService {
@Autowired
private GrayReleaseConfigRepository grayReleaseConfigRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String GRAY_CONFIG_CACHE_KEY = "gray:config:%s";
private static final long CACHE_TTL = 5 * 60 * 1000; // 5分钟
/**
* 获取灰度配置
*/
public GrayReleaseConfig getGrayConfig(String ruleCode) {
// 先从缓存获取
String cacheKey = String.format(GRAY_CONFIG_CACHE_KEY, ruleCode);
GrayReleaseConfig config = (GrayReleaseConfig) redisTemplate.opsForValue().get(cacheKey);
if (config != null) {
return config;
}
// 从数据库获取
config = grayReleaseConfigRepository.findByRuleCodeAndStatus(ruleCode, "ACTIVE");
if (config != null) {
// 缓存配置
redisTemplate.opsForValue().set(cacheKey, config, CACHE_TTL, TimeUnit.MILLISECONDS);
}
return config;
}
/**
* 创建灰度配置
*/
@Transactional
public GrayReleaseConfig createGrayConfig(GrayReleaseConfig config) {
// 检查是否已有活跃的灰度配置
GrayReleaseConfig existingConfig = grayReleaseConfigRepository.findByRuleCodeAndStatus(config.getRuleCode(), "ACTIVE");
if (existingConfig != null) {
throw new RuntimeException("Active gray release config already exists for rule: " + config.getRuleCode());
}
config.setStatus("ACTIVE");
config.setStartTime(LocalDateTime.now());
GrayReleaseConfig savedConfig = grayReleaseConfigRepository.save(config);
// 清除缓存
clearCache(config.getRuleCode());
log.info("Created gray release config: ruleCode={}, grayVersion={}, percentage={}%",
config.getRuleCode(), config.getGrayVersion(), config.getGrayPercentage());
return savedConfig;
}
/**
* 更新灰度配置
*/
@Transactional
public GrayReleaseConfig updateGrayConfig(Long id, Integer grayPercentage) {
GrayReleaseConfig config = grayReleaseConfigRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Gray release config not found"));
config.setGrayPercentage(grayPercentage);
config.setUpdatedTime(LocalDateTime.now());
GrayReleaseConfig updatedConfig = grayReleaseConfigRepository.save(config);
// 清除缓存
clearCache(config.getRuleCode());
log.info("Updated gray release config: ruleCode={}, percentage={}%",
config.getRuleCode(), grayPercentage);
return updatedConfig;
}
/**
* 完成灰度发布
*/
@Transactional
public void completeGrayRelease(String ruleCode) {
GrayReleaseConfig config = grayReleaseConfigRepository.findByRuleCodeAndStatus(ruleCode, "ACTIVE");
if (config == null) {
throw new RuntimeException("Active gray release config not found for rule: " + ruleCode);
}
config.setStatus("COMPLETED");
config.setEndTime(LocalDateTime.now());
grayReleaseConfigRepository.save(config);
// 清除缓存
clearCache(ruleCode);
log.info("Completed gray release: ruleCode={}", ruleCode);
}
/**
* 暂停灰度发布
*/
@Transactional
public void pauseGrayRelease(String ruleCode) {
GrayReleaseConfig config = grayReleaseConfigRepository.findByRuleCodeAndStatus(ruleCode, "ACTIVE");
if (config == null) {
throw new RuntimeException("Active gray release config not found for rule: " + ruleCode);
}
config.setStatus("PAUSED");
grayReleaseConfigRepository.save(config);
// 清除缓存
clearCache(ruleCode);
log.info("Paused gray release: ruleCode={}", ruleCode);
}
/**
* 清除缓存
*/
private void clearCache(String ruleCode) {
String cacheKey = String.format(GRAY_CONFIG_CACHE_KEY, ruleCode);
redisTemplate.delete(cacheKey);
}
}
3.4 规则路由器
RuleRouter.java
@Component
@Slf4j
public class RuleRouter {
@Autowired
private GrayReleaseService grayReleaseService;
@Autowired
private RuleService ruleService;
/**
* 路由规则执行
*/
public RuleExecutionResult routeRuleExecution(String ruleCode, String userId, Map<String, Object> requestData) {
// 获取灰度配置
GrayReleaseConfig grayConfig = grayReleaseService.getGrayConfig(ruleCode);
if (grayConfig == null || grayConfig.getStatus().equals("PAUSED")) {
// 没有灰度配置或已暂停,使用基础版本
return executeRule(ruleCode, grayConfig.getBaseVersion(), false, userId, requestData);
}
// 决定是否使用灰度版本
boolean useGray = shouldUseGrayVersion(grayConfig, userId);
if (useGray) {
// 使用灰度版本
return executeRule(ruleCode, grayConfig.getGrayVersion(), true, userId, requestData);
} else {
// 使用基础版本
return executeRule(ruleCode, grayConfig.getBaseVersion(), false, userId, requestData);
}
}
/**
* 决定是否使用灰度版本
*/
private boolean shouldUseGrayVersion(GrayReleaseConfig config, String userId) {
int percentage = config.getGrayPercentage();
if (percentage <= 0) {
return false;
}
if (percentage >= 100) {
return true;
}
// 基于用户ID的哈希值决定
if (userId != null) {
int hash = userId.hashCode() & Integer.MAX_VALUE;
int threshold = (int) (Integer.MAX_VALUE * (percentage / 100.0));
return hash <= threshold;
}
// 基于随机数决定
return Math.random() * 100 < percentage;
}
/**
* 执行规则
*/
private RuleExecutionResult executeRule(String ruleCode, Integer version, boolean isGray, String userId, Map<String, Object> requestData) {
long startTime = System.currentTimeMillis();
RuleExecutionResult result = new RuleExecutionResult();
try {
// 获取规则
Rule rule = ruleService.getRuleByCodeAndVersion(ruleCode, version);
if (rule == null) {
throw new RuntimeException("Rule not found: " + ruleCode + " version: " + version);
}
// 执行规则
// 这里简化处理,实际应该调用规则引擎执行
Object responseData = executeRuleContent(rule.getRuleContent(), requestData);
result.setSuccess(true);
result.setResponseData(responseData);
result.setVersion(version);
result.setIsGray(isGray);
} catch (Exception e) {
log.error("Rule execution failed: {}", e.getMessage(), e);
result.setSuccess(false);
result.setErrorMessage(e.getMessage());
result.setVersion(version);
result.setIsGray(isGray);
} finally {
result.setExecutionTime(System.currentTimeMillis() - startTime);
// 记录执行日志
logRuleExecution(ruleCode, version, isGray, userId, requestData, result);
}
return result;
}
/**
* 执行规则内容
*/
private Object executeRuleContent(String ruleContent, Map<String, Object> requestData) {
// 这里简化处理,实际应该调用规则引擎执行
// 例如使用 Drools、Easy Rules 等规则引擎
return "Rule executed successfully";
}
/**
* 记录规则执行日志
*/
private void logRuleExecution(String ruleCode, Integer version, boolean isGray,
String userId, Map<String, Object> requestData,
RuleExecutionResult result) {
// 异步记录日志,避免影响主流程
// 实际项目中应该使用消息队列或异步线程池
new Thread(() -> {
try {
RuleExecutionLog log = new RuleExecutionLog();
log.setRuleCode(ruleCode);
log.setVersion(version);
log.setIsGray(isGray);
log.setUserId(userId);
log.setRequestData(requestData != null ? new ObjectMapper().writeValueAsString(requestData) : null);
log.setResponseData(result.getResponseData() != null ? new ObjectMapper().writeValueAsString(result.getResponseData()) : null);
log.setExecutionTime(result.getExecutionTime());
log.setSuccess(result.isSuccess());
log.setErrorMessage(result.getErrorMessage());
// 保存日志到数据库
// ruleExecutionLogRepository.save(log);
} catch (Exception e) {
log.error("Failed to log rule execution", e);
}
}).start();
}
@Data
public static class RuleExecutionResult {
private boolean success;
private Object responseData;
private String errorMessage;
private Integer version;
private boolean isGray;
private long executionTime;
}
}
3.5 规则服务
RuleService.java
@Service
@Slf4j
public class RuleService {
@Autowired
private RuleRepository ruleRepository;
/**
* 获取规则
*/
public Rule getRuleById(Long id) {
return ruleRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Rule not found"));
}
/**
* 根据规则代码和版本获取规则
*/
public Rule getRuleByCodeAndVersion(String ruleCode, Integer version) {
return ruleRepository.findByRuleCodeAndVersion(ruleCode, version);
}
/**
* 获取规则的最新版本
*/
public Rule getLatestVersion(String ruleCode) {
return ruleRepository.findTopByRuleCodeOrderByVersionDesc(ruleCode);
}
/**
* 创建规则
*/
@Transactional
public Rule createRule(Rule rule) {
// 检查规则代码是否已存在
Rule existingRule = ruleRepository.findTopByRuleCodeOrderByVersionDesc(rule.getRuleCode());
if (existingRule != null) {
rule.setVersion(existingRule.getVersion() + 1);
} else {
rule.setVersion(1);
}
rule.setStatus("ACTIVE");
Rule savedRule = ruleRepository.save(rule);
log.info("Created rule: code={}, version={}", rule.getRuleCode(), rule.getVersion());
return savedRule;
}
/**
* 更新规则
*/
@Transactional
public Rule updateRule(Rule rule) {
Rule existingRule = ruleRepository.findById(rule.getId())
.orElseThrow(() -> new RuntimeException("Rule not found"));
// 检查是否需要创建新版本
if (!existingRule.getRuleContent().equals(rule.getRuleContent())) {
// 创建新版本
Rule newRule = new Rule();
newRule.setRuleName(rule.getRuleName());
newRule.setRuleCode(rule.getRuleCode());
newRule.setRuleContent(rule.getRuleContent());
newRule.setVersion(existingRule.getVersion() + 1);
newRule.setStatus("ACTIVE");
Rule savedRule = ruleRepository.save(newRule);
// 将旧版本设置为非活跃
existingRule.setStatus("INACTIVE");
ruleRepository.save(existingRule);
log.info("Updated rule: code={}, version={}", newRule.getRuleCode(), newRule.getVersion());
return savedRule;
} else {
// 只更新非内容字段
existingRule.setRuleName(rule.getRuleName());
Rule savedRule = ruleRepository.save(existingRule);
log.info("Updated rule metadata: code={}, version={}", savedRule.getRuleCode(), savedRule.getVersion());
return savedRule;
}
}
/**
* 获取规则列表
*/
public List<Rule> getRules() {
return ruleRepository.findAll();
}
/**
* 获取规则的所有版本
*/
public List<Rule> getRuleVersions(String ruleCode) {
return ruleRepository.findByRuleCodeOrderByVersionDesc(ruleCode);
}
}
3.6 控制器
RuleController.java
@RestController
@RequestMapping("/api/rules")
public class RuleController {
@Autowired
private RuleService ruleService;
@Autowired
private GrayReleaseService grayReleaseService;
@Autowired
private RuleRouter ruleRouter;
/**
* 执行规则
*/
@PostMapping("/execute/{ruleCode}")
public ResponseEntity<RuleRouter.RuleExecutionResult> executeRule(
@PathVariable String ruleCode,
@RequestParam(required = false) String userId,
@RequestBody Map<String, Object> requestData) {
RuleRouter.RuleExecutionResult result = ruleRouter.routeRuleExecution(ruleCode, userId, requestData);
return ResponseEntity.ok(result);
}
/**
* 获取规则列表
*/
@GetMapping
public ResponseEntity<List<Rule>> getRules() {
List<Rule> rules = ruleService.getRules();
return ResponseEntity.ok(rules);
}
/**
* 获取规则详情
*/
@GetMapping("/{id}")
public ResponseEntity<Rule> getRule(@PathVariable Long id) {
Rule rule = ruleService.getRuleById(id);
return ResponseEntity.ok(rule);
}
/**
* 创建规则
*/
@PostMapping
public ResponseEntity<Rule> createRule(@RequestBody Rule rule) {
Rule createdRule = ruleService.createRule(rule);
return ResponseEntity.status(HttpStatus.CREATED).body(createdRule);
}
/**
* 更新规则
*/
@PutMapping("/{id}")
public ResponseEntity<Rule> updateRule(@PathVariable Long id, @RequestBody Rule rule) {
rule.setId(id);
Rule updatedRule = ruleService.updateRule(rule);
return ResponseEntity.ok(updatedRule);
}
/**
* 获取规则版本
*/
@GetMapping("/versions/{ruleCode}")
public ResponseEntity<List<Rule>> getRuleVersions(@PathVariable String ruleCode) {
List<Rule> versions = ruleService.getRuleVersions(ruleCode);
return ResponseEntity.ok(versions);
}
}
GrayReleaseController.java
@RestController
@RequestMapping("/api/gray")
public class GrayReleaseController {
@Autowired
private GrayReleaseService grayReleaseService;
@Autowired
private RuleService ruleService;
/**
* 创建灰度配置
*/
@PostMapping
public ResponseEntity<GrayReleaseConfig> createGrayConfig(@RequestBody GrayReleaseConfig config) {
// 验证规则版本
Rule grayRule = ruleService.getRuleByCodeAndVersion(config.getRuleCode(), config.getGrayVersion());
if (grayRule == null) {
return ResponseEntity.badRequest().body(null);
}
Rule baseRule = ruleService.getRuleByCodeAndVersion(config.getRuleCode(), config.getBaseVersion());
if (baseRule == null) {
return ResponseEntity.badRequest().body(null);
}
GrayReleaseConfig createdConfig = grayReleaseService.createGrayConfig(config);
return ResponseEntity.status(HttpStatus.CREATED).body(createdConfig);
}
/**
* 更新灰度配置
*/
@PutMapping("/{id}")
public ResponseEntity<GrayReleaseConfig> updateGrayConfig(
@PathVariable Long id,
@RequestParam Integer grayPercentage) {
GrayReleaseConfig updatedConfig = grayReleaseService.updateGrayConfig(id, grayPercentage);
return ResponseEntity.ok(updatedConfig);
}
/**
* 完成灰度发布
*/
@PostMapping("/complete/{ruleCode}")
public ResponseEntity<Void> completeGrayRelease(@PathVariable String ruleCode) {
grayReleaseService.completeGrayRelease(ruleCode);
return ResponseEntity.ok().build();
}
/**
* 暂停灰度发布
*/
@PostMapping("/pause/{ruleCode}")
public ResponseEntity<Void> pauseGrayRelease(@PathVariable String ruleCode) {
grayReleaseService.pauseGrayRelease(ruleCode);
return ResponseEntity.ok().build();
}
}
四、前端实现
4.1 灰度配置页面
gray-config.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Gray Release Config</title>
<link rel="stylesheet" th:href="@{/webjars/bootstrap/5.3.0/css/bootstrap.min.css}">
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">Gray Release Configuration</h1>
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">Create Gray Release Config</h5>
</div>
<div class="card-body">
<form id="grayConfigForm">
<div class="mb-3">
<label for="ruleCode" class="form-label">Rule Code</label>
<input type="text" class="form-control" id="ruleCode" name="ruleCode" required>
</div>
<div class="mb-3">
<label for="grayVersion" class="form-label">Gray Version</label>
<input type="number" class="form-control" id="grayVersion" name="grayVersion" required>
</div>
<div class="mb-3">
<label for="baseVersion" class="form-label">Base Version</label>
<input type="number" class="form-control" id="baseVersion" name="baseVersion" required>
</div>
<div class="mb-3">
<label for="grayPercentage" class="form-label">Gray Percentage (%)</label>
<input type="range" class="form-range" id="grayPercentage" name="grayPercentage" min="0" max="100" value="1" step="1">
<div class="mt-2">
<span id="percentageValue">1%</span>
</div>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title">Active Gray Configs</h5>
</div>
<div class="card-body">
<table class="table table-striped" id="grayConfigsTable">
<thead>
<tr>
<th>Rule Code</th>
<th>Gray Version</th>
<th>Base Version</th>
<th>Percentage</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Data will be populated by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
<script th:src="@{/webjars/jquery/3.6.0/jquery.min.js}"></script>
<script th:src="@{/webjars/bootstrap/5.3.0/js/bootstrap.min.js}"></script>
<script>
$(document).ready(function() {
// 更新百分比显示
$('#grayPercentage').on('input', function() {
$('#percentageValue').text($(this).val() + '%');
});
// 提交表单
$('#grayConfigForm').submit(function(e) {
e.preventDefault();
const formData = {
ruleCode: $('#ruleCode').val(),
grayVersion: parseInt($('#grayVersion').val()),
baseVersion: parseInt($('#baseVersion').val()),
grayPercentage: parseInt($('#grayPercentage').val())
};
$.ajax({
url: '/api/gray',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(formData),
success: function(data) {
alert('Gray config created successfully');
loadGrayConfigs();
},
error: function(xhr) {
alert('Error creating gray config: ' + xhr.responseText);
}
});
});
// 加载灰度配置
function loadGrayConfigs() {
// 这里应该调用 API 获取灰度配置
// 简化处理,使用模拟数据
const mockData = [
{ id: 1, ruleCode: 'PROMOTION_RULE', grayVersion: 2, baseVersion: 1, grayPercentage: 10, status: 'ACTIVE' },
{ id: 2, ruleCode: 'PRICING_RULE', grayVersion: 3, baseVersion: 2, grayPercentage: 50, status: 'ACTIVE' }
];
const tbody = $('#grayConfigsTable tbody');
tbody.empty();
mockData.forEach(config => {
const row = `
<tr>
<td>${config.ruleCode}</td>
<td>${config.grayVersion}</td>
<td>${config.baseVersion}</td>
<td>
<input type="range" class="form-range" min="0" max="100" value="${config.grayPercentage}"
data-id="${config.id}">
<span>${config.grayPercentage}%</span>
</td>
<td>${config.status}</td>
<td>
<button class="btn btn-sm btn-success complete-btn" data-code="${config.ruleCode}">Complete</button>
<button class="btn btn-sm btn-warning pause-btn" data-code="${config.ruleCode}">Pause</button>
</td>
</tr>
`;
tbody.append(row);
});
// 绑定事件
$('.form-range').on('change', function() {
const id = $(this).data('id');
const percentage = $(this).val();
$(this).next('span').text(percentage + '%');
// 更新灰度百分比
$.ajax({
url: `/api/gray/${id}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({ grayPercentage: parseInt(percentage) }),
success: function() {
console.log('Gray percentage updated');
}
});
});
$('.complete-btn').on('click', function() {
const ruleCode = $(this).data('code');
if (confirm('Are you sure to complete gray release?')) {
$.ajax({
url: `/api/gray/complete/${ruleCode}`,
type: 'POST',
success: function() {
alert('Gray release completed');
loadGrayConfigs();
}
});
}
});
$('.pause-btn').on('click', function() {
const ruleCode = $(this).data('code');
if (confirm('Are you sure to pause gray release?')) {
$.ajax({
url: `/api/gray/pause/${ruleCode}`,
type: 'POST',
success: function() {
alert('Gray release paused');
loadGrayConfigs();
}
});
}
});
}
// 初始加载
loadGrayConfigs();
});
</script>
</body>
</html>
4.2 规则执行页面
rule-execution.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Rule Execution</title>
<link rel="stylesheet" th:href="@{/webjars/bootstrap/5.3.0/css/bootstrap.min.css}">
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">Rule Execution</h1>
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">Execute Rule</h5>
</div>
<div class="card-body">
<form id="executionForm">
<div class="mb-3">
<label for="ruleCode" class="form-label">Rule Code</label>
<input type="text" class="form-control" id="ruleCode" name="ruleCode" value="PROMOTION_RULE" required>
</div>
<div class="mb-3">
<label for="userId" class="form-label">User ID</label>
<input type="text" class="form-control" id="userId" name="userId" value="user123">
</div>
<div class="mb-3">
<label for="requestData" class="form-label">Request Data (JSON)</label>
<textarea class="form-control" id="requestData" name="requestData" rows="5">{
"productId": "P123",
"price": 100,
"quantity": 2,
"userId": "user123"
}</textarea>
</div>
<button type="submit" class="btn btn-primary">Execute</button>
</form>
</div>
</div>
<div class="card" id="resultCard" style="display: none;">
<div class="card-header">
<h5 class="card-title">Execution Result</h5>
</div>
<div class="card-body">
<div class="mb-3">
<strong>Success:</strong> <span id="successResult"></span>
</div>
<div class="mb-3">
<strong>Version:</strong> <span id="versionResult"></span>
</div>
<div class="mb-3">
<strong>Gray:</strong> <span id="grayResult"></span>
</div>
<div class="mb-3">
<strong>Execution Time:</strong> <span id="timeResult"></span> ms
</div>
<div class="mb-3">
<strong>Response:</strong>
<pre id="responseResult"></pre>
</div>
<div class="mb-3" id="errorDiv" style="display: none;">
<strong>Error:</strong> <span id="errorResult"></span>
</div>
</div>
</div>
</div>
<script th:src="@{/webjars/jquery/3.6.0/jquery.min.js}"></script>
<script th:src="@{/webjars/bootstrap/5.3.0/js/bootstrap.min.js}"></script>
<script>
$(document).ready(function() {
$('#executionForm').submit(function(e) {
e.preventDefault();
const ruleCode = $('#ruleCode').val();
const userId = $('#userId').val();
const requestData = JSON.parse($('#requestData').val());
$.ajax({
url: `/api/rules/execute/${ruleCode}`,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData),
dataType: 'json',
success: function(result) {
$('#resultCard').show();
$('#successResult').text(result.success);
$('#versionResult').text(result.version);
$('#grayResult').text(result.isGray);
$('#timeResult').text(result.executionTime);
$('#responseResult').text(JSON.stringify(result.responseData, null, 2));
if (result.success) {
$('#errorDiv').hide();
} else {
$('#errorDiv').show();
$('#errorResult').text(result.errorMessage);
}
},
error: function(xhr) {
alert('Error executing rule: ' + xhr.responseText);
}
});
});
});
</script>
</body>
</html>
五、生产级实现
5.1 配置文件
application.yml
# 应用配置
spring:
application:
name: rule-gray-release-demo
# 数据源配置
datasource:
url: jdbc:h2:mem:ruledb
driver-class-name: org.h2.Driver
username: sa
password:
# JPA 配置
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: update
show-sql: true
# H2 Console 配置
h2:
console:
enabled: true
path: /h2-console
# Redis 配置
redis:
host: localhost
port: 6379
database: 0
# 服务器配置
server:
port: 8080
servlet:
context-path: /
# 灰度发布配置
gray:
# 缓存配置
cache:
ttl: 300000 # 5分钟
# 执行日志
log:
enabled: true
batch-size: 100
# 监控
monitor:
enabled: true
interval: 60000 # 1分钟
# 监控配置
management:
endpoints:
web:
exposure:
include: "health,info,metrics,prometheus"
endpoint:
health:
show-details: always
# 日志配置
logging:
level:
com.example.rule: info
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
5.2 安全配置
1. 访问控制
- 实现基于角色的访问控制
- 限制灰度配置的管理权限
- 记录操作审计日志
2. 数据安全
- 规则内容加密存储
- 敏感信息脱敏
- 防止 SQL 注入
3. 并发控制
- 实现乐观锁
- 防止并发修改冲突
- 确保灰度配置的一致性
5.3 部署方案
Docker 部署
version: '3.8'
services:
rule-gray-release-demo:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
volumes:
redis-data:
Dockerfile
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/rule-gray-release-demo-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
六、最佳实践
6.1 灰度发布最佳实践
1. 灰度策略
- 从小比例开始:1% -> 5% -> 10% -> 25% -> 50% -> 100%
- 设置合理的观察期:每个阶段至少观察 1-2 天
- 制定明确的回滚策略:出现问题时快速回滚
2. 监控与告警
- 实时监控灰度效果:规则执行成功率、响应时间
- 设置关键指标告警:错误率、性能异常
- 对比灰度与非灰度的差异:确保灰度版本表现更好
3. 数据收集
- 收集用户反馈:了解灰度版本的用户体验
- 分析业务指标:转化率、点击率等
- 记录系统指标:CPU、内存、IO 等
6.2 流量切分最佳实践
1. 切分策略
- 基于用户 ID:确保用户体验的一致性
- 基于随机数:实现真正的随机分配
- 基于地域:适应区域性业务需求
2. 性能考虑
- 缓存灰度配置:减少数据库查询
- 异步处理:避免影响主流程
- 批量操作:减少网络开销
3. 一致性保证
- 确保同一用户始终使用同一版本
- 避免版本切换导致的用户体验不一致
- 处理缓存失效的情况
6.3 风险控制最佳实践
1. 回滚机制
- 快速回滚:一键回滚到基础版本
- 自动化回滚:当指标异常时自动回滚
- 回滚测试:定期测试回滚流程
2. 异常处理
- 优雅降级:当灰度版本异常时自动切换到基础版本
- 错误隔离:确保灰度版本的错误不会影响基础版本
- 异常监控:及时发现和处理异常
3. 合规性
- 数据隐私:确保灰度发布符合数据隐私法规
- 审计记录:记录所有灰度发布操作
- 合规检查:定期进行合规性检查
七、案例分析
7.1 案例一:电商促销规则灰度发布
场景:
- 电商平台需要更新促销规则
- 新规则可能影响促销效果和系统性能
- 需要确保规则变更不会影响现有业务
解决方案:
- 创建新的促销规则版本
- 配置 1% 的灰度流量
- 监控灰度版本的执行情况
- 逐步扩大灰度范围:1% -> 5% -> 10% -> 25% -> 50% -> 100%
- 确认无问题后完成灰度发布
效果:
- 促销规则平滑过渡
- 系统性能稳定
- 促销效果符合预期
- 上线风险显著降低
7.2 案例二:金融风控规则调整
场景:
- 金融平台需要调整风控规则
- 新规则可能影响风控效果和用户体验
- 需要确保规则变更符合监管要求
解决方案:
- 制定详细的灰度发布计划
- 配置基于用户的灰度流量
- 密切监控风控效果和用户反馈
- 进行合规性审查
- 分阶段实施灰度发布
效果:
- 风控效果稳定
- 用户体验良好
- 符合监管要求
- 系统运行正常
7.3 案例三:物流路由规则优化
场景:
- 物流平台需要优化路由规则
- 新规则可能影响配送效率和成本
- 需要快速验证规则效果
解决方案:
- 创建新的路由规则版本
- 配置 5% 的灰度流量
- 对比灰度与非灰度的配送效率
- 基于数据反馈调整规则
- 快速扩大灰度范围
效果:
- 配送效率显著提升
- 配送成本降低
- 系统性能稳定
- 上线过程平滑
八、未来发展趋势
8.1 技术演进
1. 智能化灰度发布
- 基于 AI 的灰度策略推荐
- 自动调整灰度比例
- 智能风险评估
2. 可视化灰度管理
- 实时灰度效果可视化
- 灰度配置的图形化管理
- 灰度流程的自动化编排
3. 云原生支持
- 容器化部署
- 微服务架构
- 云平台集成
8.2 应用扩展
1. 跨系统灰度
- 多系统协同灰度
- 端到端灰度流程
- 全链路监控
2. 多维度灰度
- 基于用户属性的灰度
- 基于业务场景的灰度
- 基于设备类型的灰度
3. 实时规则引擎
- 实时规则执行
- 事件驱动规则
- 流式规则处理
8.3 行业应用
1. 金融行业
- 风控规则灰度
- 定价规则灰度
- 合规规则灰度
2. 电商行业
- 促销规则灰度
- 推荐规则灰度
- 搜索规则灰度
3. 物流行业
- 路由规则灰度
- 配送规则灰度
- 计费规则灰度
小结
本文介绍了 SpringBoot 应用中实现规则灰度发布和百分比流量切分的完整解决方案,包括:
- 灰度发布原理:通过小范围验证降低规则变更风险
- 流量切分实现:基于百分比的流量分配策略
- 核心组件:灰度配置服务、规则路由器、规则执行服务
- 生产级实现:监控、告警、安全考虑
- 案例分析:电商促销、金融风控、物流路由
- 最佳实践:灰度策略、监控与告警、风险控制
- 未来趋势:智能化、可视化、云原生
通过实施这些技术方案,您可以建立一套安全、可控的规则发布系统,使新规则先对小部分用户生效,验证无误后再全量发布,从而降低上线风险,确保业务的稳定运行。
互动话题
- 您在项目中遇到过哪些规则发布的挑战?是如何解决的?
- 您对本文介绍的灰度发布策略有什么改进建议?
- 您认为在微服务架构中,规则灰度发布有哪些新的挑战?
- 您对未来规则发布技术的发展有什么看法?
欢迎在评论区分享您的经验和看法!
标题:SpringBoot + 规则灰度发布 + 百分比流量切分:新规则先对 1% 用户生效,验证无误再全量
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/07/1772777289046.html
公众号:服务端技术精选
- 导语
- 一、灰度发布的概念与原理
- 1.1 什么是灰度发布
- 1.2 灰度发布的优势
- 1.3 灰度发布的策略
- 二、技术方案设计
- 2.1 架构设计
- 2.2 核心组件
- 2.3 技术选型
- 三、核心实现
- 3.1 依赖配置
- 3.2 数据模型
- 3.3 灰度配置服务
- 3.4 规则路由器
- 3.5 规则服务
- 3.6 控制器
- 四、前端实现
- 4.1 灰度配置页面
- 4.2 规则执行页面
- 五、生产级实现
- 5.1 配置文件
- 5.2 安全配置
- 5.3 部署方案
- 六、最佳实践
- 6.1 灰度发布最佳实践
- 6.2 流量切分最佳实践
- 6.3 风险控制最佳实践
- 七、案例分析
- 7.1 案例一:电商促销规则灰度发布
- 7.2 案例二:金融风控规则调整
- 7.3 案例三:物流路由规则优化
- 八、未来发展趋势
- 8.1 技术演进
- 8.2 应用扩展
- 8.3 行业应用
- 小结
- 互动话题
评论