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 核心组件

  1. 规则路由器:根据灰度配置将请求路由到新规则或旧规则
  2. 灰度配置服务:管理灰度发布的配置,如灰度比例、灰度用户等
  3. 规则管理服务:管理规则的版本和生命周期
  4. 规则执行服务:执行规则并返回结果
  5. 监控服务:监控规则执行情况和灰度效果

2.3 技术选型

技术版本用途
SpringBoot2.7.14应用框架
Spring Data JPA-数据访问
Redis7.0+缓存和分布式锁
H2 Database-嵌入式数据库
Spring Cloud Sleuth-分布式追踪
Micrometer1.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. 创建新的促销规则版本
  2. 配置 1% 的灰度流量
  3. 监控灰度版本的执行情况
  4. 逐步扩大灰度范围:1% -> 5% -> 10% -> 25% -> 50% -> 100%
  5. 确认无问题后完成灰度发布

效果

  • 促销规则平滑过渡
  • 系统性能稳定
  • 促销效果符合预期
  • 上线风险显著降低

7.2 案例二:金融风控规则调整

场景

  • 金融平台需要调整风控规则
  • 新规则可能影响风控效果和用户体验
  • 需要确保规则变更符合监管要求

解决方案

  1. 制定详细的灰度发布计划
  2. 配置基于用户的灰度流量
  3. 密切监控风控效果和用户反馈
  4. 进行合规性审查
  5. 分阶段实施灰度发布

效果

  • 风控效果稳定
  • 用户体验良好
  • 符合监管要求
  • 系统运行正常

7.3 案例三:物流路由规则优化

场景

  • 物流平台需要优化路由规则
  • 新规则可能影响配送效率和成本
  • 需要快速验证规则效果

解决方案

  1. 创建新的路由规则版本
  2. 配置 5% 的灰度流量
  3. 对比灰度与非灰度的配送效率
  4. 基于数据反馈调整规则
  5. 快速扩大灰度范围

效果

  • 配送效率显著提升
  • 配送成本降低
  • 系统性能稳定
  • 上线过程平滑

八、未来发展趋势

8.1 技术演进

1. 智能化灰度发布

  • 基于 AI 的灰度策略推荐
  • 自动调整灰度比例
  • 智能风险评估

2. 可视化灰度管理

  • 实时灰度效果可视化
  • 灰度配置的图形化管理
  • 灰度流程的自动化编排

3. 云原生支持

  • 容器化部署
  • 微服务架构
  • 云平台集成

8.2 应用扩展

1. 跨系统灰度

  • 多系统协同灰度
  • 端到端灰度流程
  • 全链路监控

2. 多维度灰度

  • 基于用户属性的灰度
  • 基于业务场景的灰度
  • 基于设备类型的灰度

3. 实时规则引擎

  • 实时规则执行
  • 事件驱动规则
  • 流式规则处理

8.3 行业应用

1. 金融行业

  • 风控规则灰度
  • 定价规则灰度
  • 合规规则灰度

2. 电商行业

  • 促销规则灰度
  • 推荐规则灰度
  • 搜索规则灰度

3. 物流行业

  • 路由规则灰度
  • 配送规则灰度
  • 计费规则灰度

小结

本文介绍了 SpringBoot 应用中实现规则灰度发布和百分比流量切分的完整解决方案,包括:

  • 灰度发布原理:通过小范围验证降低规则变更风险
  • 流量切分实现:基于百分比的流量分配策略
  • 核心组件:灰度配置服务、规则路由器、规则执行服务
  • 生产级实现:监控、告警、安全考虑
  • 案例分析:电商促销、金融风控、物流路由
  • 最佳实践:灰度策略、监控与告警、风险控制
  • 未来趋势:智能化、可视化、云原生

通过实施这些技术方案,您可以建立一套安全、可控的规则发布系统,使新规则先对小部分用户生效,验证无误后再全量发布,从而降低上线风险,确保业务的稳定运行。

互动话题

  1. 您在项目中遇到过哪些规则发布的挑战?是如何解决的?
  2. 您对本文介绍的灰度发布策略有什么改进建议?
  3. 您认为在微服务架构中,规则灰度发布有哪些新的挑战?
  4. 您对未来规则发布技术的发展有什么看法?

欢迎在评论区分享您的经验和看法!


标题:SpringBoot + 规则灰度发布 + 百分比流量切分:新规则先对 1% 用户生效,验证无误再全量
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/07/1772777289046.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消