ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Sentinel Dashboard 1.8 持久化改造实战:Redis 规则存储全链路指南

Sentinel Dashboard 1.8 持久化改造实战:Redis 规则存储全链路指南 简介本资源为 Sentinel 1.8 版本 Dashboard 源码的 Nacos 持久化改造工程面向微服务架构下需实现流控与降级规则集中管理的中高级 Java 开发者及运维工程师。它解决了原生 Sentinel-Dashboard 规则仅内存存储、重启丢失、多实例配置不一致等核心痛点通过深度集成 Nacos 实现动态规则持久化、实时监听与跨环境共享。资源共657个文件涵盖144个Java业务类如FlowControllerV1、GatewayFlowRuleController、163个编译后class、112个XML配置与Spring Boot相关配置、98个JS/74个HTML前端交互文件以及properties、json等元数据文件整体压缩包24.73MB结构完整、模块清晰便于二次开发与调试。已有1101人学习下载提供可直接编译运行的完整改造方案包含Nacos客户端接入、RefreshScope动态刷新、FlowRuleManager/DegradeRuleManager规则同步逻辑、ConfigService监听回调实现及打包部署说明助读者快速掌握服务治理中配置中心与流量控制的协同实践。1. 为什么改 Sentinel Dashboard 1.8 的源码必须做持久化不是配个 Redis 就完事了Sentinel Dashboard 1.8 默认所有规则流控、降级、热点、系统保护、授权全部存在内存里——服务一重启所有配置全丢。这在生产环境是不可接受的运维同学深夜改完限流阈值凌晨发布新版本早上发现规则没了灰度环境刚调好的热点参数切流量前被误删甚至开发本地调试时反复启动 Spring Boot每次都要重填表单。很多人以为“加个 Redis 就能持久化”但实际跑起来会发现Dashboard 本身不主动写 Redis规则下发到客户端后也不自动回写控制台刷新后页面显示空/v2/flow/rule接口返回[]日志里只有No rule repository found。根本原因在于 Sentinel 1.8 的 Dashboard 是一个纯前端管理界面 内存规则仓库的组合体它不内置任何持久化模块也不提供开箱即用的数据库适配器。真正要落地必须修改其sentinel-dashboard模块的源码替换掉InMemoryRuleRepository接入可读写的规则存储层。适合正在搭建高可用微服务治理平台、需要审计规则变更历史、或已将 Sentinel 集成进 CI/CD 流程的中大型团队——不是为了“能存”而是为了“可追溯、可回滚、可协同”。2. 替换 InMemoryRuleRepository从源码结构到持久化接口设计Sentinel Dashboard 1.8 的规则管理核心逻辑集中在com.alibaba.csp.sentinel.dashboard.rule包下。要实现持久化不能简单 patch 几个类而需理解其分层设计RuleRepositoryT是抽象仓储接口InMemoryRuleRepository是默认实现所有 Controller如FlowControllerV2通过Autowired注入该接口完全解耦。因此最干净的改造方式是提供一个新的RedisRuleRepository实现并通过 Spring Profile 或Primary控制 Bean 注入优先级而非直接修改原有类。2.1 源码定位与关键类职责分析进入sentinel-dashboard模块的src/main/java/com/alibaba/csp/sentinel/dashboard/rule/目录重点关注以下类RuleRepositoryT泛型接口定义save,saveAll,delete,findAll,load五个方法是持久化的契约入口InMemoryRuleRepository内存实现使用ConcurrentHashMap存储load()方法直接返回new ArrayList(map.values())FlowRuleRepository/DegradeRuleRepository等继承InMemoryRuleRepositoryFlowRule仅做类型包装无额外逻辑FlowControllerV2REST 控制器调用flowRuleRepository.save()等方法不关心底层存储介质。提示不要修改FlowControllerV2中的业务逻辑。所有持久化改动应严格限定在RuleRepository实现层。否则后续升级 Dashboard 版本时控制器变更会导致 merge 冲突剧增。2.2 定义 Redis 规则序列化协议Redis 不是文档数据库无法直接存 Java 对象。必须定义统一序列化格式确保不同语言客户端如 Go/Sentinel Go也能解析。Sentinel 官方推荐 JSON 格式且要求 key 结构清晰、可检索。我们采用如下约定规则类型Redis Key 模式Value 格式示例 Key流控规则sentinel:rule:flow:{app}:{id}JSON array ofFlowRulesentinel:rule:flow:order-service:1001降级规则sentinel:rule:degrade:{app}:{id}JSON array ofDegradeRulesentinel:rule:degrade:user-service:2001热点规则sentinel:rule:param_flow:{app}:{id}JSON array ofParamFlowRulesentinel:rule:param_flow:pay-service:3001其中{app}是应用名来自MachineInfo.appName{id}是规则唯一 ID由 Dashboard 生成非自增主键。不使用 Hash 结构因需支持按应用批量查询、按 ID 精确删除List/Hash 均不如 String 命名空间灵活。2.3 实现 RedisRuleRepository以 FlowRule 为例Component ConditionalOnProperty(name sentinel.rule.store.type, havingValue redis, matchIfMissing false) public class RedisFlowRuleRepository implements RuleRepositoryFlowRule { private final StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper; public RedisFlowRuleRepository(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) { this.redisTemplate redisTemplate; this.objectMapper objectMapper; } Override public FlowRule save(FlowRule entity) { if (entity null || StringUtils.isBlank(entity.getApp())) { throw new IllegalArgumentException(App name cannot be null); } String key buildKey(entity.getApp(), entity.getRuleId()); try { String json objectMapper.writeValueAsString(entity); redisTemplate.opsForValue().set(key, json); return entity; } catch (JsonProcessingException e) { throw new RuntimeException(Failed to serialize FlowRule, e); } } Override public ListFlowRule findAll() { // 扫描所有 flow 规则 key生产环境慎用 SCAN此处为简化演示 SetString keys redisTemplate.keys(sentinel:rule:flow:*); if (keys null || keys.isEmpty()) { return Collections.emptyList(); } ListFlowRule rules new ArrayList(); for (String key : keys) { String json redisTemplate.opsForValue().get(key); if (json ! null !json.trim().isEmpty()) { try { FlowRule rule objectMapper.readValue(json, FlowRule.class); rules.add(rule); } catch (JsonProcessingException e) { // 记录 warn 日志跳过损坏数据 log.warn(Invalid FlowRule JSON in key: {}, key, e); } } } return rules; } Override public void delete(Long id) { // 注意id 是 ruleId 字符串非 Long 类型Sentinel 1.8 中 ruleId 是 String // 此处需根据实际存储结构调整真实实现应查出 app 名再构造 key throw new UnsupportedOperationException(Delete by ID not supported in Redis impl); } // 其他方法省略完整实现需覆盖 saveAll/deleteAll/load 等 private String buildKey(String app, String ruleId) { return String.format(sentinel:rule:flow:%s:%s, app, ruleId); } }注意delete(Long id)方法在 Redis 实现中不能仅靠id删除因为id在 Redis key 中与app组合存在。真实场景需先通过app查询所有规则再遍历匹配ruleId或改用HSCANHDEL。此处抛出异常是为了强制上层Controller调用deleteByAppAndId(app, id)—— 这正是 Dashboard 1.8 原生FlowControllerV2.delete方法签名说明框架已预留扩展点。2.4 配置多规则类型共用同一套 Redis 实现Sentinel Dashboard 要求每种规则类型流控/降级/热点都有独立的RuleRepositoryBean。若为每种类型都写一个 Redis 实现代码重复率极高。正确做法是抽取通用基类public abstract class AbstractRedisRuleRepositoryT implements RuleRepositoryT { protected final StringRedisTemplate redisTemplate; protected final ObjectMapper objectMapper; protected final String ruleType; // flow, degrade, param_flow protected AbstractRedisRuleRepository(StringRedisTemplate redisTemplate, ObjectMapper objectMapper, String ruleType) { this.redisTemplate redisTemplate; this.objectMapper objectMapper; this.ruleType ruleType; } protected String buildKey(String app, String ruleId) { return String.format(sentinel:rule:%s:%s:%s, ruleType, app, ruleId); } // 实现 save/find/load 等通用逻辑 }然后让RedisFlowRuleRepository、RedisDegradeRuleRepository分别继承它并传入对应ruleType。这样既满足 Spring 容器对不同类型 Bean 的注入需求又避免 80% 代码复制。3. 集成 Redis 并启用持久化application.yml 配置与启动验证完成RuleRepository实现后需让 Dashboard 启动时加载 Redis 版本而非内存版。Sentinel 1.8 默认使用Profile(!redis)控制内存实现因此只需激活redisprofile 并配置连接参数。3.1 application.yml 关键配置项spring: profiles: active: redis # 必须显式激活否则 InMemoryRuleRepository 仍生效 redis: host: 127.0.0.1 port: 6379 password: database: 0 timeout: 5000 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1ms sentinel: rule: store: type: redis # 触发 ConditionalOnProperty提示sentinel.rule.store.type是我们自定义的配置项用于ConditionalOnProperty判断。它不依赖 Sentinel 官方配置完全由你控制。若未来升级到 Sentinel 2.x此配置名可平滑迁移。3.2 启动时 Bean 注入优先级验证启动 Dashboard 后检查日志是否输出o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean flowRuleRepository with a different definition该日志表明RedisFlowRuleRepository已成功替换InMemoryFlowRuleRepository。若未出现检查Component是否添加在 Redis 实现类上ConditionalOnProperty的havingValue是否与application.yml中值完全一致区分大小写spring.profiles.active是否正确设置不能写成spring.profiles.default。3.3 通过 REST API 验证持久化效果使用 curl 发送一条流控规则curl -X POST http://localhost:8080/v2/flow/rule \ -H Content-Type: application/json \ -d [ { app: test-app, ip: 127.0.0.1, port: 8719, rule: { resource: GET:/api/user, limitApp: default, grade: 1, count: 10.0, strategy: 0, refResource: , controlBehavior: 0, warmUpPeriodSec: 10, maxQueueingTimeMs: 500, burstCount: 0, durationInSec: 1, minRequestCount: 5, windowIntervalMs: 1000, ruleId: flow-001 } } ]然后立即执行# 查看 Redis 中是否写入 redis-cli GET sentinel:rule:flow:test-app:flow-001 # 应返回类似 JSON 字符串 {resource:GET:/api/user,limitApp:default,grade:1,count:10.0,strategy:0,controlBehavior:0,warmUpPeriodSec:10,maxQueueingTimeMs:500,burstCount:0,durationInSec:1,minRequestCount:5,windowIntervalMs:1000,ruleId:flow-001,app:test-app} # 重启 Dashboard 进程 # 再请求 /v2/flow/rule确认规则仍在 curl http://localhost:8080/v2/flow/rule?apptest-app若重启后/v2/flow/rule返回非空数组且内容与 Redis 中一致则持久化链路打通。3.4 生产环境必须调整的 Redis 操作策略上述findAll()使用KEYS命令在大数据量 Redis 中会阻塞主线程严禁在生产环境使用。必须替换为SCAN游标分页Override public ListFlowRule findAll() { ListFlowRule rules new ArrayList(); Cursorbyte[] cursor null; try { cursor redisTemplate.getConnectionFactory() .getConnection() .scan(ScanOptions.scanOptions() .match(sentinel:rule:flow:*) .count(1000) .build()); while (cursor.hasNext()) { byte[] keyBytes cursor.next(); String key new String(keyBytes, StandardCharsets.UTF_8); String json redisTemplate.opsForValue().get(key); if (json ! null) { rules.add(objectMapper.readValue(json, FlowRule.class)); } } } catch (Exception e) { log.error(SCAN failed, e); } finally { if (cursor ! null) { try { cursor.close(); } catch (IOException ignored) {} } } return rules; }注意SCAN不保证原子性若规则在扫描过程中被删除可能返回null若并发写入可能漏扫。这是分布式存储的固有特性Dashboard 作为管理端可接受最终一致性。4. 解决三大高频问题规则丢失、ID 冲突、跨集群同步持久化上线后常遇到三类典型问题它们不源于代码 bug而是架构设计盲区。4.1 问题一规则保存后立即消失或 Dashboard 页面显示为空现象POST/v2/flow/rule返回 200但刷新页面规则不见/v2/flow/rule接口返回空数组。根因Dashboard 1.8 的规则推送机制与持久化存在时序冲突。当用户点击“新增”按钮前端先调POST /v2/flow/rule后端save()写入 Redis紧接着前端发起GET /v2/flow/rule?appxxx请求后端findAll()从 Redis 读取。若 Redis 主从同步延迟 100ms从节点尚未同步findAll()就读到空结果。解决方案强制读主节点Redis 6或使用READONLY NO选项// 在 RedisRuleRepository 构造函数中注入 RedisConnection private RedisConnection getConnection() { RedisConnectionFactory factory redisTemplate.getConnectionFactory(); RedisConnection conn factory.getConnection(); // 强制使用主节点连接 if (conn instanceof RedisStandaloneConnection) { // Standalone 模式无需处理 } else if (conn instanceof RedisClusterConnection) { // Cluster 模式下ensureMaster() 无效改用 EVAL 脚本保证读写同节点 } return conn; }更稳妥的做法是在save()方法末尾主动触发一次findAll()并缓存结果 1 秒供后续GET请求直接返回绕过 Redis 延迟。这是 Dashboard 场景下的合理妥协。4.2 问题二多个 Dashboard 实例同时操作导致 ruleId 冲突现象A 实例新增规则flow-001B 实例也生成flow-001写入 Redis 时后者覆盖前者。根因ruleId由前端 JavaScript 生成Date.now() Math.random()非全局唯一。Sentinel 1.8 未提供 ID 生成服务。解决方案在save()方法中接管 ID 生成使用 Redis INCR 原子计数器private String generateRuleId(String app, String ruleType) { String counterKey sentinel:rule:id:counter: app : ruleType; Long id redisTemplate.opsForValue().increment(counterKey); return String.format(%s-%s-%d, ruleType, app, id); }调用位置save(FlowRule entity)方法开头若entity.getRuleId()为空则调用generateRuleId()赋值。这样所有实例共享同一计数器ID 绝对唯一。4.3 问题三多套环境dev/test/prod共用一套 Redis规则互相污染现象测试环境删了一条规则生产环境 Dashboard 也同步消失了。根因key 命名未隔离环境。当前sentinel:rule:flow:{app}:{id}中{app}可能相同如都叫user-service。解决方案在 key 中加入spring.profiles.active前缀private String buildKey(String app, String ruleId) { String profile environment.getProperty(spring.profiles.active, default); return String.format(sentinel:rule:%s:%s:%s:%s, ruleType, profile, app, ruleId); }同时application-dev.yml中配置spring.profiles.active: devapplication-prod.yml中配置prod。这样dev和prod环境的 key 完全隔离互不影响。5. 进阶技巧用 Lua 脚本保证规则原子性更新与版本校验当规则需支持“编辑并保存”即先查旧规则、再改字段、再存时单纯GET SET有并发风险用户 A 读取规则 v1用户 B 同时读取 v1 并修改为 v2 写入A 再把修改后的 v1 写入导致 B 的变更丢失。Sentinel Dashboard 1.8 的FlowControllerV2.update()方法正是这种模式。5.1 设计带版本号的规则存储结构修改FlowRule类增加version字段Long 类型初始值为 0。每次保存时version自增public class FlowRule extends AbstractRule { private Long version 0L; // getter/setter }Redis 中 value 仍为 JSON但包含version: 1字段。5.2 编写 Lua 脚本实现 CASCompare-And-Swap将save()和update()合并为一个 Lua 脚本保证“读-改-写”原子性-- update_rule.lua -- KEYS[1] rule_key, ARGV[1] new_json, ARGV[2] expected_version local old_json redis.call(GET, KEYS[1]) if old_json false then -- 规则不存在直接 SET redis.call(SET, KEYS[1], ARGV[1]) return 1 else local old_obj cjson.decode(old_json) if old_obj.version tonumber(ARGV[2]) then redis.call(SET, KEYS[1], ARGV[1]) return 1 else return 0 -- 版本不匹配拒绝更新 end endJava 调用public Boolean updateWithVersion(String key, String newJson, Long expectedVersion) { DefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptText(luaScriptContent); // 加载上面的 Lua script.setResultType(Long.class); Long result redisTemplate.execute(script, Collections.singletonList(key), newJson, expectedVersion.toString()); return result 1L; }5.3 在 Controller 中集成版本校验修改FlowControllerV2.update()PostMapping(/rule/{id}) ResponseStatus(HttpStatus.OK) public Result? update(PathVariable(id) Long id, RequestBody FlowRule rule) { // 1. 先从 Redis 读取原规则含 version FlowRule oldRule flowRuleRepository.load(id); // 需扩展 load 方法 if (oldRule null) { return Result.ofFail(-1, rule does not exist); } // 2. 新规则 version old 1 rule.setVersion(oldRule.getVersion() 1); // 3. 调用 Lua 脚本更新 boolean success redisFlowRuleRepository.updateWithVersion( buildKey(rule.getApp(), rule.getRuleId()), objectMapper.writeValueAsString(rule), oldRule.getVersion() ); if (!success) { return Result.ofFail(-1, concurrent update conflict, please refresh and retry); } return Result.ok(); }提示前端需在编辑表单中隐藏version字段并在提交时携带。Dashboard 页面加载规则时自动将version填入 hidden input。这样用户看到的是“编辑-保存”底层是强一致的乐观锁更新。至此Sentinel Dashboard 1.8 的持久化改造已覆盖从源码切入、Redis 集成、生产调优到并发控制的全链路。你得到的不是一个临时 patch而是一个可随 Sentinel 版本演进、可对接 MySQL/Elasticsearch、可支撑百节点集群的规则治理底座。本文还有配套的精品资源点击获取
返回列表