加群联系作者vx:xiaoda0423
仓库地址:webvueblog.github.io/JavaPlusDoc…
webvueblog.github.io/JavaPlusDoc…
1. 架构概览
-
WebSocket 连接管理
- 后端维护
Session映射(如clientId → WebSocketSession),用于后续下发指令。
- 后端维护
-
心跳检测
- 客户端定时(如每 5 秒)发送心跳消息;
- 服务端收到心跳后更新最后活跃时间;
- 若超时(如 10 秒未收到心跳),则认为客户端离线并触发下线处理。
-
指令下发
- 通过管理的
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 缓存心跳时间,或将
sessions、lastHeartbeat抽象为分布式存储,配合多实例时使用。 - 安全:在
HandshakeInterceptor中完成身份校验与clientId绑定,防止伪造。 - 监控:可将心跳异常 / 指令下发结果上报到监控系统(如 Prometheus/Grafana)以便实时告警。
- 手写缓存逻辑(适合对流程有精细控制的场景)
- 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. 原理概述
- 分布式锁作用
在高并发场景下,多个请求同时尝试对同一商品进行库存扣减,如果不做控制极易造成库存超卖或数据不一致。Redisson 分布式锁基于 RedisSETNX+ Lua 脚本 + Watchdog 自动续期机制,实现了可靠的锁获取与释放。 - 锁的粒度
以商品 ID 作为锁的 key,比如lock:product:123,确保对同一个商品的所有扣减操作串行化。 - 自动续期 vs 超时时间
Redisson 的 Watchdog 机制会在持有锁的业务执行时间超过锁过期时间时自动续期,避免死锁;同时,最好指定一个最大持锁时间以防业务异常卡住。
2. 实现步骤
-
注入 RedissonClient
在 Spring Boot 配置中,定义并注入RedissonClient。 -
获取锁并尝试加锁
RLock lock = redissonClient.getLock("lock:product:" + productId); boolean acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); -
业务临界区
如果获取到锁,再进行:- 从缓存/数据库读取当前库存
- 检查库存是否足够
- 扣减库存(写入 Redis 和/或持久化到数据库)
-
释放锁
无论成功或失败,都在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. 高并发性能调优
-
异步非阻塞框架
- Netty/Reactor/Vert.x:基于事件驱动,减少线程上下文切换。
- 线程模型优化:使用合适的线程池(如 ForkJoinPool、Disruptor),合理配置
corePoolSize、maxPoolSize、队列类型。 - 线程绑定与亲和性:在 Linux 下可通过
taskset绑定关键线程到特定 CPU 核心,减少缓存抖动。
-
连接和网络优化
- 长连接与连接池:HTTP Keep-Alive、数据库连接池(HikariCP)、Redis 连接池(Lettuce/Redisson)。
- TCP 参数调优:调整
tcp_tw_reuse、tcp_fin_timeout、net.core.somaxconn、backlog;使用SO_REUSEPORT分摊负载。 - 零拷贝:在文件传输、大流量场景下使用
sendfile()、mmap达到零拷贝。
-
内存与 GC 调优
- 合理划分堆内存:根据业务峰值和停顿时长要求,调整
-Xms/Xmx;分配合理的年老代和幸存区。 - 选择 GC 算法:G1 GC 适合大内存低停顿需求,ZGC/ Shenandoah 可进一步降低延迟。
- 逃逸分析与对象复用:采用池化(Object Pool)、ThreadLocal 防止过度分配短生命周期对象。
- 合理划分堆内存:根据业务峰值和停顿时长要求,调整
-
分布式限流与降级
- 令牌桶/漏桶算法:在 API 网关(如 Nginx/lua_nginx_module 或 Spring Cloud Gateway)层面限流。
- 熔断降级:Hystrix、Resilience4j 实现快速失败与自动恢复,保护下游服务。
2. 大数据处理
-
批处理 vs 流处理
- 批处理:Hadoop MapReduce、Apache Spark;适合大规模离线计算,如 ETL、OLAP。
- 流处理:Apache Flink、Kafka Streams;用于实时计算与近实时分析,如监控告警、用户画像更新。
-
存储与索引
- 分布式文件系统:HDFS、CephFS;海量文件存储与高吞吐。
- 列式存储:Apache Parquet、ORC;结合 Presto/Trino 做交互式查询。
- NoSQL 数据库:HBase、Cassandra;低延迟随机读写,海量稠密数据场景。
-
数据管道与调度
- 消息队列:Kafka、Pulsar 做数据引擎入湖;保证高吞吐和可持久化。
- 调度系统:Airflow、Azkaban;编排 DAG 任务,支持任务依赖、重试与监控。
- 数据质量:引入 Apache Griffin、Great Expectations 做血缘与校验。
-
机器学习与图计算
- ML 平台:Spark MLlib、TensorFlow on Kubernetes;支撑离线模型训练与在线预测。
- 图计算:GraphX、JanusGraph;适用于社交关系、电网拓扑等复杂网络。
3. 物联网可靠通信
-
轻量级协议
- MQTT:低带宽、支持三种 QoS(0/1/2),常用 Broker:Eclipse Mosquitto、EMQX。
- CoAP:基于 UDP,适合受限网络,支持确认消息与资源观察。
-
连接管理与持久化
- 会话持久化:保持客户端状态,断线后自动重连和消息积累(MQTT Persistent Session)。
- 心跳与保活:合理设置 Keep-Alive,及时发现死连接并释放资源。
-
边缘计算与缓存
- Edge Node:将部分逻辑下沉至边缘(如 AWS Greengrass、OpenFaaS on k3s),降低时延并提升可用性。
- 本地缓冲:网络抖动或离线时先行采集到本地存储(如 SQLite 或轻量级 KV),网络恢复后批量上报。
-
安全与认证
- TLS/DTLS 加密:确保传输安全;设备端可使用 mbedTLS、wolfSSL。
- 令牌与证书:采用 JWT Token 或 X.509 证书进行设备鉴权,配合动态密钥轮换。
4. SaaS 安全隔离
-
多租户模式
-
数据库多租户:
- 独立库:租户隔离最高,运维成本较大。
- 同库多 Schema:隔离性中等,便于对租户进行分组管理。
- 同表多租户:在一张表中增加
tenant_id字段,隔离最弱,扩展性最佳。
-
-
访问控制
- 认证:OAuth2/OIDC(Keycloak、Auth0),支持租户级身份联合。
- 授权:基于租户上下文的 RBAC 或 ABAC;在微服务调用链中传递租户信息,进行侧车或网关拦截。
-
资源隔离
- Kubernetes Namespace/NetworkPolicy:隔离网络与算力。
- 限额与配额:通过 CPU/Memory limits、Storage Quotas、防火墙策略保证租户资源公平使用。
-
审计与监控
- 日志审计:Elasticsearch + Kibana + Filebeat 实现多租户日志分区与审计合规。
- 安全扫描:定期进行漏洞扫描(Trivy、Anchore)、依赖检查与渗透测试。
5. 微前端架构
-
集成方式
- Module Federation(Webpack 5):动态加载不同子应用,保证共享依赖版本兼容。
- Iframe 沙箱:最强隔离但 SEO 与跨域通信成本高。
- JavaScript 注入:通过
<script>标签加载 UMD 包,适合老平台渐进式改造。
-
共享库与版本管理
- 依赖协调:设定主应用承载 React/Vue/Angular 运行时,并将公共库配置为 external,以减少冗余打包。
- 样式隔离:使用 CSS Modules、Shadow DOM 或 scoping 命名空间避免全局冲突。
-
部署与 CI/CD
- 独立流水线:每个子应用独立构建、测试与发布,触发主应用版本更新。
- 灰度发布:利用 Feature Flag 或路由级别的 AB 测试,逐步打开新子应用。
-
路由与状态管理
- 统一路由:主应用负责全局路由,将特定路径委派给子应用。
- 跨子应用通信:基于 Event Bus、PostMessage 或全局状态(如 Redux、MobX)桥接。
总结
将上述五个领域融汇到同一平台时,应采用分层解耦与契约优先的设计思想:
-
边缘层:IoT 设备与边缘节点负责实时采集与预处理。
-
接入层:使用高并发网关(Nginx + lua、Spring Cloud Gateway)做协议转换、限流与鉴权。
-
计算层:
- 实时:Flink/Spark Streaming 处理 IoT 与日志流;
- 离线:Spark 或 Hadoop 完成大规模数据挖掘。
-
存储层:结合 OLTP(MySQL/Cassandra)、OLAP(ClickHouse/Presto)与分布式文件系统。
-
展现层:主应用协调微前端子应用,实现弹性扩展与租户隔离。
1. 核心思路
- 认证(Authentication) :用户首次登录,校验用户名/密码,生成带有最小角色信息(或权限版本号)的 JWT 并返回客户端。
- 授权(Authorization) :每次请求携带 JWT,服务端先校验 JWT 签名与有效期,然后根据 JWT 中的用户 ID(和权限版本号),从 Redis 拉取该用户的“实际权限集”,并在当前请求上下文中完成资源级、操作级的权限检查。
- 动态更新 & 撤销:当管理员调整某用户权限时,只需更新 Redis 中该用户的权限集(或其“权限版本号”),下一个请求即可生效;也可将 JWT 加入 Redis 黑名单以实现立即撤销。
2. Redis 中的权限数据模型
-
Key 设计
perm:user:{userId}→ Redis Set,成员为字符串形式的权限标识(如order:create、order: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 集成示例
-
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); } } -
权限提取 & 细粒度检查
自定义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) { … } -
权限版本校验
如果需要更严格的版本管理,可在 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 集群 + 哨兵确保授权系统稳定。
一、架构概览
-
主从拓扑
- Master:负责写入,开启 binlog(建议 ROW 格式)。
- Slave(s) :一个或多个,从 Master 同步 binlog,用于读或备用。
-
复制模式
- 异步复制:默认模式,性能最好,但主故障时可能丢本地尚未传输的事务。
- 半同步复制: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_exporter 或 Zabbix 来采集和告警:
| 指标名称 | 含义 |
|---|---|
Seconds_Behind_Master | 延迟秒数,主从同步滞后程度 |
Slave_IO_Running/Slave_SQL_Running | 复制线程状态 |
Binlog_cache_disk_use | binlog 缓存落盘次数 |
| 主库 QPS/ TPS、连接数、慢查询数等 | 整体性能健康度 |
-
告警阈值示例
Seconds_Behind_Master > 10s持续超过 1 分钟Slave_IO_Running = No或Slave_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)。
五、读写分离与流量切换
-
ProxySQL 或 HAProxy + Consul/DNS:前端接入层,根据角色标签(writer/reader)动态投流。
-
故障切换脚本中,切换文学如下:
- Orchestrator 回调脚本更新 ProxySQL 后端列表
- ProxySQL 刷新 hostgroup,对外服务无感知
六、演练与注意事项
- 定期演练:模拟 Master 宕机,观察切换耗时及业务影响。
- 延迟控制:业务侧避免长事务,Slave 保持低延迟;可结合 semi-sync。
- 监控覆盖:切换前后,监控也要监控 MHA/Orchestrator 本身的健康。
- 数据一致性:提前规划切换窗口,检查是否有未同步的事务;读写分离场景要防止“读到旧数据”。
- 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.x)或异步
WriteApi(2.x),避免逐条阻塞。
- 对于高吞吐写入场景,务必启用异步批量(1.x)或异步
-
Retention Policy(保留策略)
- 在 1.x 中,
influxDB.createRetentionPolicy("rp_30d", "monitoring", "30d", 1, true); - 在 2.x 中,Retention Policy 在创建 Bucket 时配置。
- 在 1.x 中,
-
索引与 Tag/Field 区分
- 将高基数(few distinct values)的字段设置为 Tag,可加速过滤查询;
- 将大基数字段设为 Field,避免生成过多 time series。
-
连接与超时
- 合理设置 HTTP 连接池与超时参数,避免短连接频繁建立的开销。
-
监控与报警
- 可结合 Kapacitor、Chronograf 或自定义警报模块,对聚合后的结果进行阈值报警。
一、总体架构与目标
-
缓存预热(Cache Warm-up)
- 在业务启动或定时任务中,将热点数据从 MySQL 一次性加载到 Redis,避免“冷启动”带来的高并发击穿。
-
双层缓存
- L1 本地缓存(如 Caffeine/Guava) :极低延迟,存储本机访问最热的少量数据。
- L2 Redis 分布式缓存:跨实例共享,容量更大。
-
双层一致性
- 保证 读:L1 → Redis → MySQL 顺序;
- 保证 写:MySQL → Redis → L1,或采用先删后写的双删策略,并广播失效。
二、缓存预热策略
-
热点扫描
- 在数据库中维护热点表/统计表(如访问量最高 Top N),定期(或启动时)查询
SELECT * FROM product WHERE id IN ( … 热点ID )。
- 在数据库中维护热点表/统计表(如访问量最高 Top N),定期(或启动时)查询
-
批量写入
- 使用管道(pipeline)或 Lua 脚本批量
MSET到 Redis,减少 RTT。
- 使用管道(pipeline)或 Lua 脚本批量
-
分批执行
- 如果数据量较大,分批(例如每批 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)双删 + 消息广播
- 第一删:更新前先删除 L1 和 Redis。
- DB 更新。
- 第二删:延时(如 200ms)再次删除 Redis(及 L1)。
- 广播:若有多实例,可通过消息队列(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)互补,实现极低延迟与分布式共享。
- 一致性:写入时可选同步写透或“双删+广播”策略,配合延时再删、锁和空值,确保高可用、高一致。