分布式微服务系统架构第139集:和后端字节沟通技术

278 阅读23分钟

加群联系作者vx:xiaoda0423

仓库地址:webvueblog.github.io/JavaPlusDoc…

1024bat.cn/

github.com/webVueBlog/…

webvueblog.github.io/JavaPlusDoc…


1. 架构概览

  1. WebSocket 连接管理

    • 后端维护 Session 映射(如 clientId → WebSocketSession),用于后续下发指令。
  2. 心跳检测

    • 客户端定时(如每 5 秒)发送心跳消息;
    • 服务端收到心跳后更新最后活跃时间;
    • 若超时(如 10 秒未收到心跳),则认为客户端离线并触发下线处理。
  3. 指令下发

    • 通过管理的 Session 映射,服务端可随时向在线客户端推送 JSON 格式的指令。
    • 对接上游业务触发点(如 Kafka 消息、RPC 调用)即可调用下发方法。

2. 依赖与配置

pom.xml 中引入:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

然后在 application.yml 中(可选)配置:

spring:
  websocket:
    enabled: true

3. WebSocket 配置

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
          .addHandler(new CommandWebSocketHandler(), "/ws/command")
          .setAllowedOrigins("*");
    }
}

4. Handler 核心代码

@Component
public class CommandWebSocketHandler extends TextWebSocketHandler {
    // 存活会话映射
    private final ConcurrentMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    // 最后心跳时间
    private final ConcurrentMap<String, Instant> lastHeartbeat = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String clientId = getClientId(session);
        sessions.put(clientId, session);
        lastHeartbeat.put(clientId, Instant.now());
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage msg) throws Exception {
        JsonNode node = new ObjectMapper().readTree(msg.getPayload());
        String type = node.get("type").asText();
        String clientId = getClientId(session);

        if ("heartbeat".equals(type)) {
            // 更新心跳
            lastHeartbeat.put(clientId, Instant.now());
        } else if ("ack".equals(type)) {
            // 处理客户端对指令的确认
        } else {
            // 其它上行消息
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        String clientId = getClientId(session);
        sessions.remove(clientId);
        lastHeartbeat.remove(clientId);
    }

    // 下发指令
    public boolean sendCommand(String clientId, Object command) {
        WebSocketSession session = sessions.get(clientId);
        if (session != null && session.isOpen()) {
            try {
                String payload = new ObjectMapper().writeValueAsString(command);
                session.sendMessage(new TextMessage(payload));
                return true;
            } catch (IOException e) {
                // 日志 & 异常处理
            }
        }
        return false;
    }

    private String getClientId(WebSocketSession session) {
        // 从 URL 参数、Header 或首次消息中获取 clientId
        return (String) session.getAttributes().get("clientId");
    }
}

5. 心跳监控调度

@Component
public class HeartbeatMonitor {
    private static final Duration TIMEOUT = Duration.ofSeconds(10);
    private final CommandWebSocketHandler handler;

    public HeartbeatMonitor(CommandWebSocketHandler handler) {
        this.handler = handler;
    }

    @Scheduled(fixedRate = 5000)
    public void checkClients() {
        Instant now = Instant.now();
        handler.lastHeartbeat.forEach((clientId, lastTime) -> {
            if (Duration.between(lastTime, now).compareTo(TIMEOUT) > 0) {
                // 超时:下线处理
                handler.sessions.remove(clientId);
                handler.lastHeartbeat.remove(clientId);
                // 可触发离线事件、通知其他系统等
            }
        });
    }
}

注意:需要在启动类或配置类上加上 @EnableScheduling


6. 客户端示例(JavaScript)

const socket = new WebSocket("wss://yourdomain.com/ws/command?clientId=DEVICE_123");
socket.onopen = () => {
  // 启动心跳
  setInterval(() => {
    socket.send(JSON.stringify({ type: "heartbeat" }));
  }, 5000);
};

socket.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === "command") {
    // 处理下发的指令
    // … 执行完成后可回 ack:
    socket.send(JSON.stringify({ type: "ack", commandId: msg.commandId }));
  }
};

7. 小结与扩展

  • 秒级感知:客户端 5s 心跳 + 服务端 5s 检测 → 最多 10s 超时感知,可按需调短间隔到 1s。
  • 高并发:可结合 Redis 缓存心跳时间,或将 sessionslastHeartbeat 抽象为分布式存储,配合多实例时使用。
  • 安全:在 HandshakeInterceptor 中完成身份校验与 clientId 绑定,防止伪造。
  • 监控:可将心跳异常 / 指令下发结果上报到监控系统(如 Prometheus/Grafana)以便实时告警。
  1. 手写缓存逻辑(适合对流程有精细控制的场景)
  2. Spring Cache + CompositeCacheManager(借助框架,配置化、易扩展)

1. 手写缓存流程

public class MyService {
    // 本地缓存(Caffeine)
    private final Cache<String, MyEntity> localCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build();

    // Redis 缓存模板
    private final StringRedisTemplate redis;
    private static final String REDIS_PREFIX = "myEntity:";

    private final MyEntityRepository repository;

    public MyService(StringRedisTemplate redis, MyEntityRepository repository) {
        this.redis = redis;
        this.repository = repository;
    }

    public MyEntity getById(String id) {
        // 1. 先查本地
        MyEntity data = localCache.getIfPresent(id);
        if (data != null) {
            return data;
        }

        // 2. 再查 Redis
        String json = redis.opsForValue().get(REDIS_PREFIX + id);
        if (json != null) {
            data = parse(json);
            // 回填本地
            localCache.put(id, data);
            return data;
        }

        // 3. 最后落到 DB
        data = repository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException(id));

        // 同步写入 Redis & 本地
        String toCache = serialize(data);
        redis.opsForValue().set(REDIS_PREFIX + id, toCache, 10, TimeUnit.MINUTES);
        localCache.put(id, data);
        return data;
    }

    private MyEntity parse(String json) { /* … */ }
    private String serialize(MyEntity e) { /* … */ }
}

优化和注意点

  • 缓存穿透:对不存在的 key,可在本地/Redis 分别缓存一个空对象(或布隆过滤器拦截)。
  • 缓存雪崩:为不同缓存设置随机过期时间。
  • 缓存击穿:热门 key 丢失时,可加互斥锁或使用 Caffeine 的 refreshAfterWrite 异步刷新。
  • 并发写入:对写操作可采用双写(先写 DB,成功后再写缓存);也可用 Redis 事务或 Lua 脚本保证原子性。

2. Spring Cache + CompositeCacheManager

Spring Boot 中可以同时注册本地和 Redis 两个 CacheManager,并通过 CompositeCacheManager 按顺序查找:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager mgr = new CaffeineCacheManager("entities");
        mgr.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(5, TimeUnit.MINUTES));
        return mgr;
    }

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration cfg = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
            );
        return RedisCacheManager.builder(factory)
            .cacheDefaults(cfg)
            .build();
    }

    @Bean
    public CacheManager compositeCacheManager(
            @Qualifier("caffeineCacheManager") CacheManager caffeine,
            @Qualifier("redisCacheManager")   CacheManager redis) {
        CompositeCacheManager mgr = new CompositeCacheManager(caffeine, redis);
        // 如果都没命中,返回 NoOpCache 而不是抛异常
        mgr.setFallbackToNoOpCache(true);
        return mgr;
    }
}

然后在 Service 中直接使用注解:

@Service
public class MyService {
    @Cacheable(cacheNames = "entities", key = "#id")
    public MyEntity getById(String id) {
        // 如果本地和 Redis 都没命中,才执行下面的 DB 查询
        return repository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException(id));
    }

    @CacheEvict(cacheNames = "entities", key = "#id")
    public void update(MyEntity e) {
        repository.save(e);
        // 调用后本地和 Redis 缓存都会被清除,保证下一次读时一致
    }
}

优势

  • 声明式:只需在方法上贴注解,缓存逻辑自动管理。
  • 可链式:先查 Caffeine,再查 Redis,再执行方法。
  • 统一管理:Miss / Evict 统一由 Spring Cache 框架完成,易于扩展其他缓存策略。

小结

  • 手写缓存 灵活,可深度定制穿透、击穿、雪崩等策略;
  • Spring Cache + CompositeCacheManager 更加配置化,适合快速集成和维护。

1. 原理概述

  1. 分布式锁作用
    在高并发场景下,多个请求同时尝试对同一商品进行库存扣减,如果不做控制极易造成库存超卖或数据不一致。Redisson 分布式锁基于 Redis SETNX + Lua 脚本 + Watchdog 自动续期机制,实现了可靠的锁获取与释放。
  2. 锁的粒度
    以商品 ID 作为锁的 key,比如 lock:product:123,确保对同一个商品的所有扣减操作串行化。
  3. 自动续期 vs 超时时间
    Redisson 的 Watchdog 机制会在持有锁的业务执行时间超过锁过期时间时自动续期,避免死锁;同时,最好指定一个最大持锁时间以防业务异常卡住。

2. 实现步骤

  1. 注入 RedissonClient
    在 Spring Boot 配置中,定义并注入 RedissonClient

  2. 获取锁并尝试加锁

    RLock lock = redissonClient.getLock("lock:product:" + productId);
    boolean acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
    
  3. 业务临界区
    如果获取到锁,再进行:

    • 从缓存/数据库读取当前库存
    • 检查库存是否足够
    • 扣减库存(写入 Redis 和/或持久化到数据库)
  4. 释放锁
    无论成功或失败,都在 finally 块中 unlock(),防止死锁。


3. 示例代码

@Service
public class ProductService {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private ProductRepository productRepository;  // JPA 或 MyBatis 等

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 扣减商品库存
     *
     * @param productId 商品ID
     * @param amount    扣减数量
     * @return 是否扣减成功
     */
    public boolean decreaseStock(Long productId, int amount) {
        String lockKey = "lock:product:" + productId;
        // 可选:使用公平锁,保证先请求先获得
        RLock lock = redissonClient.getLock(lockKey);

        boolean locked = false;
        try {
            // 最长等待2秒获取锁,持锁最大10秒(Watchdog 自动续期)
            locked = lock.tryLock(2, 10, TimeUnit.SECONDS);
            if (!locked) {
                // 获取锁失败,直接返回或重试
                return false;
            }

            // ========== 业务临界区 ==========
            // 1. 从 Redis 读取库存
            String stockKey = "stock:product:" + productId;
            String stockStr = redisTemplate.opsForValue().get(stockKey);
            int stock = stockStr == null ? loadStockFromDb(productId) : Integer.parseInt(stockStr);

            if (stock < amount) {
                // 库存不足
                return false;
            }

            // 2. 扣减库存
            int newStock = stock - amount;
            redisTemplate.opsForValue().set(stockKey, String.valueOf(newStock));

            // 3. 异步或同步更新数据库
            productRepository.updateStock(productId, newStock);
            return true;

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private int loadStockFromDb(Long productId) {
        Product p = productRepository.findById(productId)
                        .orElseThrow(() -> new RuntimeException("商品不存在"));
        // 同步到 Redis
        redisTemplate.opsForValue().set("stock:product:" + productId, String.valueOf(p.getStock()));
        return p.getStock();
    }
}

4. 注意事项

  • 锁键设计:务必将锁 key 与业务唯一标识(这里是商品 ID)组合,避免不同商品互相干扰。
  • 超时时间tryLock(waitTime, leaseTime, timeUnit) 中的 leaseTime 应大于业务执行最坏情况时长;若设置为 0,则依赖 Watchdog 自动续期,但也可导致锁未及时释放。
  • 锁释放:一定要在 finally 块中释放,并 isHeldByCurrentThread() 判断,防止误释放。
  • 性能权衡:在超高并发下,大量请求排队会导致响应延迟;可结合 “限流 + 重试 + 失败快速失败” 策略优化用户体验。
  • 双写一致性:如果同时更新 Redis 缓存和数据库,需考虑缓存与 DB 的双写一致性,建议使用异步消息或 Spring 事务事件最终一致性方案。

5. 扩展思考

  • Redisson RedLock
    如果部署了多个独立的 Redis 实例,可使用 Redisson 的 getRedLock(...) 来获得更安全的分布式锁。
  • 基于 Lua 脚本的原子操作
    对于简单的库存递减,也可直接在 Redis 端用 Lua 脚本做原子判断与递减,效率更高,但不具备自动续期功能。
  • 库存削峰
    在抢购活动中,可先将库存预加载到本地内存队列或秒杀队列中,通过队列消费来削峰,再异步持久化,进一步提高系统吞吐。

1. 高并发性能调优

  1. 异步非阻塞框架

    • Netty/Reactor/Vert.x:基于事件驱动,减少线程上下文切换。
    • 线程模型优化:使用合适的线程池(如 ForkJoinPool、Disruptor),合理配置 corePoolSizemaxPoolSize、队列类型。
    • 线程绑定与亲和性:在 Linux 下可通过 taskset 绑定关键线程到特定 CPU 核心,减少缓存抖动。
  2. 连接和网络优化

    • 长连接与连接池:HTTP Keep-Alive、数据库连接池(HikariCP)、Redis 连接池(Lettuce/Redisson)。
    • TCP 参数调优:调整 tcp_tw_reusetcp_fin_timeoutnet.core.somaxconnbacklog;使用 SO_REUSEPORT 分摊负载。
    • 零拷贝:在文件传输、大流量场景下使用 sendfile()mmap 达到零拷贝。
  3. 内存与 GC 调优

    • 合理划分堆内存:根据业务峰值和停顿时长要求,调整 -Xms/Xmx;分配合理的年老代和幸存区。
    • 选择 GC 算法:G1 GC 适合大内存低停顿需求,ZGC/ Shenandoah 可进一步降低延迟。
    • 逃逸分析与对象复用:采用池化(Object Pool)、ThreadLocal 防止过度分配短生命周期对象。
  4. 分布式限流与降级

    • 令牌桶/漏桶算法:在 API 网关(如 Nginx/lua_nginx_module 或 Spring Cloud Gateway)层面限流。
    • 熔断降级:Hystrix、Resilience4j 实现快速失败与自动恢复,保护下游服务。

2. 大数据处理

  1. 批处理 vs 流处理

    • 批处理:Hadoop MapReduce、Apache Spark;适合大规模离线计算,如 ETL、OLAP。
    • 流处理:Apache Flink、Kafka Streams;用于实时计算与近实时分析,如监控告警、用户画像更新。
  2. 存储与索引

    • 分布式文件系统:HDFS、CephFS;海量文件存储与高吞吐。
    • 列式存储:Apache Parquet、ORC;结合 Presto/Trino 做交互式查询。
    • NoSQL 数据库:HBase、Cassandra;低延迟随机读写,海量稠密数据场景。
  3. 数据管道与调度

    • 消息队列:Kafka、Pulsar 做数据引擎入湖;保证高吞吐和可持久化。
    • 调度系统:Airflow、Azkaban;编排 DAG 任务,支持任务依赖、重试与监控。
    • 数据质量:引入 Apache Griffin、Great Expectations 做血缘与校验。
  4. 机器学习与图计算

    • ML 平台:Spark MLlib、TensorFlow on Kubernetes;支撑离线模型训练与在线预测。
    • 图计算:GraphX、JanusGraph;适用于社交关系、电网拓扑等复杂网络。

3. 物联网可靠通信

  1. 轻量级协议

    • MQTT:低带宽、支持三种 QoS(0/1/2),常用 Broker:Eclipse Mosquitto、EMQX。
    • CoAP:基于 UDP,适合受限网络,支持确认消息与资源观察。
  2. 连接管理与持久化

    • 会话持久化:保持客户端状态,断线后自动重连和消息积累(MQTT Persistent Session)。
    • 心跳与保活:合理设置 Keep-Alive,及时发现死连接并释放资源。
  3. 边缘计算与缓存

    • Edge Node:将部分逻辑下沉至边缘(如 AWS Greengrass、OpenFaaS on k3s),降低时延并提升可用性。
    • 本地缓冲:网络抖动或离线时先行采集到本地存储(如 SQLite 或轻量级 KV),网络恢复后批量上报。
  4. 安全与认证

    • TLS/DTLS 加密:确保传输安全;设备端可使用 mbedTLS、wolfSSL。
    • 令牌与证书:采用 JWT Token 或 X.509 证书进行设备鉴权,配合动态密钥轮换。

4. SaaS 安全隔离

  1. 多租户模式

    • 数据库多租户

      • 独立库:租户隔离最高,运维成本较大。
      • 同库多 Schema:隔离性中等,便于对租户进行分组管理。
      • 同表多租户:在一张表中增加 tenant_id 字段,隔离最弱,扩展性最佳。
  2. 访问控制

    • 认证:OAuth2/OIDC(Keycloak、Auth0),支持租户级身份联合。
    • 授权:基于租户上下文的 RBAC 或 ABAC;在微服务调用链中传递租户信息,进行侧车或网关拦截。
  3. 资源隔离

    • Kubernetes Namespace/NetworkPolicy:隔离网络与算力。
    • 限额与配额:通过 CPU/Memory limits、Storage Quotas、防火墙策略保证租户资源公平使用。
  4. 审计与监控

    • 日志审计:Elasticsearch + Kibana + Filebeat 实现多租户日志分区与审计合规。
    • 安全扫描:定期进行漏洞扫描(Trivy、Anchore)、依赖检查与渗透测试。

5. 微前端架构

  1. 集成方式

    • Module Federation(Webpack 5):动态加载不同子应用,保证共享依赖版本兼容。
    • Iframe 沙箱:最强隔离但 SEO 与跨域通信成本高。
    • JavaScript 注入:通过 <script> 标签加载 UMD 包,适合老平台渐进式改造。
  2. 共享库与版本管理

    • 依赖协调:设定主应用承载 React/Vue/Angular 运行时,并将公共库配置为 external,以减少冗余打包。
    • 样式隔离:使用 CSS Modules、Shadow DOM 或 scoping 命名空间避免全局冲突。
  3. 部署与 CI/CD

    • 独立流水线:每个子应用独立构建、测试与发布,触发主应用版本更新。
    • 灰度发布:利用 Feature Flag 或路由级别的 AB 测试,逐步打开新子应用。
  4. 路由与状态管理

    • 统一路由:主应用负责全局路由,将特定路径委派给子应用。
    • 跨子应用通信:基于 Event Bus、PostMessage 或全局状态(如 Redux、MobX)桥接。

总结

将上述五个领域融汇到同一平台时,应采用分层解耦契约优先的设计思想:

  1. 边缘层:IoT 设备与边缘节点负责实时采集与预处理。

  2. 接入层:使用高并发网关(Nginx + lua、Spring Cloud Gateway)做协议转换、限流与鉴权。

  3. 计算层

    • 实时:Flink/Spark Streaming 处理 IoT 与日志流;
    • 离线:Spark 或 Hadoop 完成大规模数据挖掘。
  4. 存储层:结合 OLTP(MySQL/Cassandra)、OLAP(ClickHouse/Presto)与分布式文件系统。

  5. 展现层:主应用协调微前端子应用,实现弹性扩展与租户隔离。

1. 核心思路

  1. 认证(Authentication) :用户首次登录,校验用户名/密码,生成带有最小角色信息(或权限版本号)的 JWT 并返回客户端。
  2. 授权(Authorization) :每次请求携带 JWT,服务端先校验 JWT 签名与有效期,然后根据 JWT 中的用户 ID(和权限版本号),从 Redis 拉取该用户的“实际权限集”,并在当前请求上下文中完成资源级、操作级的权限检查。
  3. 动态更新 & 撤销:当管理员调整某用户权限时,只需更新 Redis 中该用户的权限集(或其“权限版本号”),下一个请求即可生效;也可将 JWT 加入 Redis 黑名单以实现立即撤销。

2. Redis 中的权限数据模型

  • Key 设计

    • perm:user:{userId} → Redis Set,成员为字符串形式的权限标识(如 order:createorder:view:{orderId}product:edit…)
    • blacklist:jwt → Redis Set,用于存储被强制失效的 JWT Token ID(jti)
  • 示例

    SADD perm:user:42 "order:create" "order:view:123" "order:view:124"
    SADD perm:user:42 "product:list" "product:edit:567"
    

3. JWT 生成:最小信息 + jti

  • Payload 示例(只包含 userId、roles、jti、permVersion)

    {
      "sub": "42",
      "roles": ["USER"],
      "jti": "8a7f1e2b-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
      "permVer": 3,
      "iat": 1685000000,
      "exp": 1685604800
    }
    
  • jti 用于在 Redis 黑名单中快速定位该 token;

  • permVer(权限版本号)可选,用于粗粒度比较:如果客户端 JWT 中的版本号 < Redis 中的最新版本号,可强制重新登录或重发最新权限的 JWT。


4. Spring Security 集成示例

  1. JWT 校验过滤器
    拦截请求、解析并验证 JWT,检查 jti 是否在黑名单中,若通过则将用户身份放入 SecurityContext

    public class JwtAuthenticationFilter extends OncePerRequestFilter {
      @Autowired private JwtUtil jwtUtil;
      @Autowired private RedisTemplate<String, Object> redis;
      
      @Override
      protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
          throws ServletException, IOException {
        String token = resolveToken(req);
        if (token != null && jwtUtil.validate(token)) {
          String jti = jwtUtil.getJti(token);
          Boolean blacklisted = redis.opsForSet().isMember("blacklist:jwt", jti);
          if (Boolean.TRUE.equals(blacklisted)) {
            res.sendError(HttpStatus.UNAUTHORIZED.value(), "Token revoked");
            return;
          }
          Long userId = jwtUtil.getUserId(token);
          // 构造一个只带身份(无权限)的 Authentication
          UsernamePasswordAuthenticationToken auth =
              new UsernamePasswordAuthenticationToken(userId, null, List.of());
          SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(req, res);
      }
    }
    
  2. 权限提取 & 细粒度检查
    自定义 PermissionEvaluator,在每次方法/接口调用时,从 Redis 拉取该用户的权限 Set,判断是否包含目标资源的操作权限。

    @Component
    public class RedisPermissionEvaluator implements PermissionEvaluator {
      @Autowired private RedisTemplate<String, Object> redis;
      
      @Override
      public boolean hasPermission(Authentication auth, Object targetDomainObject, Object perm) {
        Long userId = (Long) auth.getPrincipal();
        String permissionNeeded = buildPermString(targetDomainObject, perm);
        Set<Object> perms = redis.opsForSet().members("perm:user:" + userId);
        return perms != null && perms.contains(permissionNeeded);
      }
      // 例如把 (orderEntity, "view") → "order:view:123"
      private String buildPermString(Object obj, Object perm) { … }
    }
    

    然后在 Controller/Service 中可用

    @PreAuthorize("hasPermission(#order, 'view')")
    public OrderDto getOrder(Order order) { … }
    
  3. 权限版本校验
    如果需要更严格的版本管理,可在 JWT 校验通过后,比较 JWT 中的 permVer 与 Redis 里存的最新用户权限版本号,若不一致则拒绝或强制客户端刷新 token。


5. 权限更新 & 撤销

  • 更新用户权限
    管理后台操作时,更新 perm:user:{userId},并可同时自增一个全局或 per-user 的 permVer

  • 立即失效(撤销 JWT):

    // 将 jti 加入黑名单,并设置与 JWT 剩余过期时间相同的 TTL
    redis.opsForSet().add("blacklist:jwt", jti);
    redis.expire("blacklist:jwt", remainingSeconds, TimeUnit.SECONDS);
    

6. 性能与扩展建议

  • 缓存:大流量场景下,可在本地(JVM 缓存或 Caffeine)再加一层短时缓存,减少对 Redis 的频繁读取。
  • 批量预取:如果一次请求需校验多个资源,可一次性 SMEMBERS 后在本地判断。
  • 监控:对 Redis 命中率、JWT 验证耗时做监控,及时发现瓶颈。
  • 高可用:Redis 集群 + 哨兵确保授权系统稳定。

一、架构概览

  1. 主从拓扑

    • Master:负责写入,开启 binlog(建议 ROW 格式)。
    • Slave(s) :一个或多个,从 Master 同步 binlog,用于读或备用。
  2. 复制模式

    • 异步复制:默认模式,性能最好,但主故障时可能丢本地尚未传输的事务。
    • 半同步复制:Master 等待至少一个 Slave 收到 binlog 后再返回 ACK,丢失数据风险降低,但写性能略受影响。
    • 组复制/InnoDB Cluster:官方高可用方案,支持多主或单主自动投票,但部署和运维复杂度较高。

二、主从搭建步骤

1. Master 配置(假设 IP=10.0.0.1)

# /etc/my.cnf
[mysqld]
server-id=1
log-bin=mysql-bin           # 必须开启二进制日志
binlog-format=ROW           # 推荐 row 模式
sync_binlog=1               # 强一致性选项
innodb_flush_log_at_trx_commit=1
# 如果要半同步,还需加载插件:
plugin-load=rpl_semi_sync_master=semisync_master.so
rpl_semi_sync_master_enabled=1
rpl_semi_sync_master_timeout=1000
-- 在 Master 上创建复制帐号
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

-- 查看当前 binlog 坐标
FLUSH TABLES WITH READ LOCK;
SHOW MASTER STATUS;
-- 记录 File 和 Position,随后解锁
UNLOCK TABLES;

2. Slave 配置(假设 IP=10.0.0.2)

# /etc/my.cnf
[mysqld]
server-id=2
relay-log=relay-bin
relay-log-index=relay-bin.index
# 如果有半同步:
plugin-load=rpl_semi_sync_slave=semisync_slave.so
rpl_semi_sync_slave_enabled=1
# 从 Master 复制初始数据(可通过 xtrabackup 或 mysqldump)
mysqldump --single-transaction --master-data=2 -u root -p \
  --databases your_db > initial.sql

# 导入到 Slave
mysql -u root -p < initial.sql
-- 在 Slave 上配置复制
CHANGE MASTER TO
  MASTER_HOST='10.0.0.1',
  MASTER_USER='repl',
  MASTER_PASSWORD='repl_password',
  MASTER_LOG_FILE='mysql-bin.000001',  -- 上一步记录的 File
  MASTER_LOG_POS=12345;               -- 上一步记录的 Position

START SLAVE;
SHOW SLAVE STATUS\G;  -- 确认 Slave_IO_Running/Slave_SQL_Running 均为 Yes

三、监控关键指标

建议使用 Prometheus + mysqld_exporterZabbix 来采集和告警:

指标名称含义
Seconds_Behind_Master延迟秒数,主从同步滞后程度
Slave_IO_Running/Slave_SQL_Running复制线程状态
Binlog_cache_disk_usebinlog 缓存落盘次数
主库 QPS/ TPS、连接数、慢查询数等整体性能健康度
  • 告警阈值示例

    • Seconds_Behind_Master > 10s 持续超过 1 分钟
    • Slave_IO_Running = NoSlave_SQL_Running = No
  • 告警渠道

    • 邮件/钉钉/企业微信 Webhook
    • PagerDuty、Opsgenie 等

四、自动化故障切换方案

两种常见开源方案,各有优劣:

1. MHA (Master High Availability)

  • 原理:通过 Perl 脚本 + Agent,监控 Master 心跳;检测故障后,自动选举最“最优” Slave(最小延迟、最全数据)提升为 Master,并重定向其他 Slaves。
  • 优点:成熟、社区多,支持半同步、读写分离配合 Proxy。
  • 缺点:对网络环境、Agent 部署要求高,故障切换可能有几秒中断。

配置示例(简化版)

# /etc/mha.cnf (管理端)
[server default]
user=mha
passwd=mha_password
ssh_user=root
repl_user=repl
repl_password=repl_password

[db01]
hostname=10.0.0.1
port=3306

[db02]
hostname=10.0.0.2
port=3306
# 安装 MHA Manager:
yum install mha4mysql-node mha4mysql-manager

# 推送 SSH key、安装 agent 到各节点
# 启动监控
masterha_manager --conf=/etc/mha.cnf --remove_dead_master_conf --ignore_last_failover

2. Orchestrator

  • 原理:由 GitHub 上的 Orchestrator 服务维护 MySQL 拓扑图,实时监控复制状态;故障时自动或手动 promote Slave,支持自定义脚本更新 DNS/Proxy。
  • 优点:Web GUI 可视化,拓扑自动发现、支持多种拓扑。
  • 缺点:初次配置稍复杂,需要额外部署 HTTP 服务和后端数据库(可用 SQLite/MySQL)。

简化配置片段(orchestrator.conf.json

{
  "MySQLTopologyUser":         "orchestrator",
  "MySQLTopologyPassword":     "orch_password",
  "MySQLOrchestratorHost":     "0.0.0.0",
  "MySQLOrchestratorPort":     3000,
  "DiscoverByShowSlaveHosts":  true,
  "FailMasterDetectionPeriod": 5,
  "RecoveryPeriodBlockSeconds":  30,
  "AutoRebalance":             true,
  "PromotionRuleOrder":        ["is_candidate","gtid_domain_id","replica_count"]
}
# 启动 Orchestrator
orchestrator --config=/path/to/orchestrator.conf.json \
             --debug
  • 在 Web 界面中添加集群,开启 Auto–failover
  • 故障触发后,Orchestrator 会执行 promotion,并可调用外部脚本(更新 ProxySQL 或 DNS)。

五、读写分离与流量切换

  • ProxySQLHAProxy + Consul/DNS:前端接入层,根据角色标签(writer/reader)动态投流。

  • 故障切换脚本中,切换文学如下:

    1. Orchestrator 回调脚本更新 ProxySQL 后端列表
    2. ProxySQL 刷新 hostgroup,对外服务无感知

六、演练与注意事项

  1. 定期演练:模拟 Master 宕机,观察切换耗时及业务影响。
  2. 延迟控制:业务侧避免长事务,Slave 保持低延迟;可结合 semi-sync
  3. 监控覆盖:切换前后,监控也要监控 MHA/Orchestrator 本身的健康。
  4. 数据一致性:提前规划切换窗口,检查是否有未同步的事务;读写分离场景要防止“读到旧数据”。
  • InfluxDB 1.x:使用社区广泛的 influxdb-java 客户端
  • InfluxDB 2.x:使用官方推荐的 influxdb-client-java

一、环境准备

1. Maven 依赖

1.1 InfluxDB 1.x 客户端(influxdb-java)

<dependency>
  <groupId>org.influxdb</groupId>
  <artifactId>influxdb-java</artifactId>
  <version>2.23</version>  <!-- 请根据最新版本调整 -->
</dependency>

1.2 InfluxDB 2.x 客户端(influxdb-client-java)

<dependency>
  <groupId>com.influxdb</groupId>
  <artifactId>influxdb-client-java</artifactId>
  <version>6.0.0</version> <!-- 请根据最新版本调整 -->
</dependency>

二、连接到 InfluxDB

2.1 1.x 版连接

import org.influxdb.InfluxDB;
import org.influxdb.InfluxDBFactory;

String url = "http://localhost:8086";
String username = "admin";
String password = "password";
InfluxDB influxDB = InfluxDBFactory.connect(url, username, password);

// 可选:设置全局数据库
influxDB.setDatabase("monitoring");

2.2 2.x 版连接

import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory;

String url    = "http://localhost:8086";
String token  = "your-token";
String org    = "your-org";
String bucket = "your-bucket";

InfluxDBClient influxDBClient = InfluxDBClientFactory.create(url, token.toCharArray(), org, bucket);

三、写入时序数据

3.1 1.x 版:同步写入

import org.influxdb.dto.Point;

Point point = Point.measurement("cpu_usage")
    .time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
    .tag("host", "server01")
    .addField("usage", 0.85)
    .build();

influxDB.write(point);

3.2 1.x 版:批量异步写入

influxDB.enableBatch(1000, 200, TimeUnit.MILLISECONDS); 
// 第1个参数:批量大小,200毫秒内不足1000条则一起写入
// 第2:flush间隔

// 随后直接写 point 即可,客户端会自动异步批量发送
influxDB.write(Point.measurement("mem")
    .time(...).addField("free", 512L).build());

3.3 2.x 版:使用 WriteApi

import com.influxdb.client.write.Point;
import com.influxdb.client.WriteApiBlocking;

// 构造数据点
Point point = Point.measurement("cpu_usage")
    .addTag("host", "server02")
    .addField("usage", 0.65)
    .time(Instant.now(), WritePrecision.MS);

// 同步写入
WriteApiBlocking writeApi = influxDBClient.getWriteApiBlocking();
writeApi.writePoint(point);

// 或者异步批量
influxDBClient.getWriteApi().writePoint(point);

四、查询时序数据

4.1 1.x 版:使用 InfluxQL

String query = "SELECT mean("usage") FROM "cpu_usage" "
             + "WHERE time > now() - 1h GROUP BY time(1m), "host"";
QueryResult result = influxDB.query(new Query(query, "monitoring"));

for (QueryResult.Result r : result.getResults()) {
    for (QueryResult.Series series : r.getSeries()) {
        System.out.println("Host: " + series.getTags().get("host"));
        List<List<Object>> values = series.getValues();
        // values.get(i).get(0): timestamp, get(1): mean usage
    }
}

4.2 2.x 版:使用 Flux

import com.influxdb.client.QueryApi;

String flux = "from(bucket:"your-bucket")"
            + " |> range(start: -1h)"
            + " |> filter(fn: (r) => r._measurement == "cpu_usage" and r._field == "usage")"
            + " |> aggregateWindow(every: 1m, fn: mean)";

// 执行查询
QueryApi queryApi = influxDBClient.getQueryApi();
List<FluxTable> tables = queryApi.query(flux);

for (FluxTable table : tables) {
    for (FluxRecord record : table.getRecords()) {
        Instant time = record.getTime();
        Double value = record.getValueByKey("_value", Double.class);
        String host  = record.getValueByKey("host", String.class);
        System.out.printf("%s %s: %.3f%n", time, host, value);
    }
}

五、常见配置与优化

  1. 批量写入

    • 对于高吞吐写入场景,务必启用异步批量(1.x)或异步 WriteApi(2.x),避免逐条阻塞。
  2. Retention Policy(保留策略)

    • 在 1.x 中,influxDB.createRetentionPolicy("rp_30d", "monitoring", "30d", 1, true);
    • 在 2.x 中,Retention Policy 在创建 Bucket 时配置。
  3. 索引与 Tag/Field 区分

    • 将高基数(few distinct values)的字段设置为 Tag,可加速过滤查询;
    • 将大基数字段设为 Field,避免生成过多 time series。
  4. 连接与超时

    • 合理设置 HTTP 连接池与超时参数,避免短连接频繁建立的开销。
  5. 监控与报警

    • 可结合 Kapacitor、Chronograf 或自定义警报模块,对聚合后的结果进行阈值报警。

一、总体架构与目标

  1. 缓存预热(Cache Warm-up)

    • 在业务启动或定时任务中,将热点数据从 MySQL 一次性加载到 Redis,避免“冷启动”带来的高并发击穿。
  2. 双层缓存

    • L1 本地缓存(如 Caffeine/Guava) :极低延迟,存储本机访问最热的少量数据。
    • L2 Redis 分布式缓存:跨实例共享,容量更大。
  3. 双层一致性

    • 保证 :L1 → Redis → MySQL 顺序;
    • 保证 :MySQL → Redis → L1,或采用先删后写的双删策略,并广播失效。

二、缓存预热策略

  1. 热点扫描

    • 在数据库中维护热点表/统计表(如访问量最高 Top N),定期(或启动时)查询 SELECT * FROM product WHERE id IN ( … 热点ID )
  2. 批量写入

    • 使用管道(pipeline)或 Lua 脚本批量 MSET 到 Redis,减少 RTT。
  3. 分批执行

    • 如果数据量较大,分批(例如每批 1000 条)执行,以防一次性查询过重或 Redis 写入过慢。
@Service
public class CacheWarmUpService {
    @Autowired private JdbcTemplate jdbc;
    @Autowired private StringRedisTemplate redis;
    @Value("${cache.hot.ids.sql}") private String hotIdsSql;

    @PostConstruct
    public void warmUp() {
        List<Long> hotIds = jdbc.queryForList(hotIdsSql, Long.class);
        int batchSize = 500;
        for (int i = 0; i < hotIds.size(); i += batchSize) {
            List<Long> batch = hotIds.subList(i, Math.min(i + batchSize, hotIds.size()));
            List<Product> products = jdbc.query(
                "SELECT id,name,price FROM product WHERE id IN (" +
                 batch.stream().map(String::valueOf).collect(joining(",")) + ")",
                new BeanPropertyRowMapper<>(Product.class)
            );
            redis.executePipelined((RedisCallback<Object>) conn -> {
                for (Product p : products) {
                    byte[] key = ("product:" + p.getId()).getBytes();
                    byte[] val = serialize(p);
                    conn.set(key, val);
                    conn.expire(key, 3600); // 1 小时过期
                }
                return null;
            });
        }
    }
}

三、双层缓存读写流程

1. 读取流程

客户端请求 →
├─ L1 本地缓存(Caffeine)查找  
│     └─ 命中:返回  
│     └─ 未命中 → 
├─ Redis 查找  
│     └─ 命中:写入 L1 并返回  
│     └─ 未命中 → 
└─ MySQL 查询 → 写入 Redis → 写入 L1 → 返回

示例(Spring + Caffeine + Redis)

@Cacheable(cacheNames = "productL1", key = "#id")
public Product getFromL1(Long id) {
    // 该方法仅在 L1 miss 时调用,内部再查 L2/DB
    String redisKey = "product:" + id;
    byte[] data = redis.opsForValue().get(redisKey);
    if (data != null) {
        return deserialize(data, Product.class);
    }
    Product p = productRepo.findById(id).orElse(null);
    if (p != null) {
        redis.opsForValue().set(redisKey, serialize(p), 1, TimeUnit.HOURS);
    }
    return p;
}

这里 Spring Cache 作为 L1,@Cacheable 将自动把方法返回值写入本地缓存。


2. 写入/更新流程

保证写操作对三层状态的一致性,常见有两种模式:

(1)同步写入(Write-Through/Write-Behind)

  • 写透(Write-Through) :应用在写 Redis 时,同步写入 DB。
  • 写回(Write-Behind) :先写入缓存,异步刷回 DB。
  • 缺点:写入时会增加延迟,且复杂度高,一般用于对性能容忍度高、要求极强一致性的场景。

(2)双删 + 消息广播

  1. 第一删:更新前先删除 L1 和 Redis。
  2. DB 更新
  3. 第二删:延时(如 200ms)再次删除 Redis(及 L1)。
  4. 广播:若有多实例,可通过消息队列(RabbitMQ/Kafka)或 Redis Pub/Sub 通知其他实例清除 L1。
public void updateProduct(Product p) {
    String key = "product:" + p.getId();
    // 1. 第一删
    redis.delete(key);
    cacheManager.getCache("productL1").evict(p.getId());

    // 2. 更新数据库
    productRepo.save(p);

    // 3. 延时双删
    executor.schedule(() -> {
        redis.delete(key);
        cacheManager.getCache("productL1").evict(p.getId());
    }, 200, TimeUnit.MILLISECONDS);

    // 4. 广播(可选)
    redis.convertAndSend("cache-invalidate", key);
}

四、附加防护:缓存击穿与雪崩

  • 热点锁:遇到大并发同一 key miss 时,用分布式锁(Redisson)或本地互斥锁让单个线程去 DB 加载,其他线程等待。
  • 缓存空值:对不存在的数据,用空对象或布隆过滤器拦截,防止 DB 穿透。
  • TTL 随机化:为缓存设置随机过期时间,避免大规模同一时刻失效的雪崩。

五、总结

  • 预热:启动或定时将热点一次性写入 Redis,保证“热”数据即时可读。
  • 双层:L1(本地)+ L2(Redis)互补,实现极低延迟与分布式共享。
  • 一致性:写入时可选同步写透或“双删+广播”策略,配合延时再删、锁和空值,确保高可用、高一致。