Redis持久化:RDB与AOF的权衡

一、引子:Redis宕机后数据去哪了? Redis是内存数据库,数据存储在内存中。那么问题来了:Redis宕机后,数据还在吗? 1.1 场景:双11大促数据丢失事故 场景:电商网站,双11大促 00:00:00 - 大促开始,流量暴增 00:00:01 - Redis缓存:10万个商品详情、50万个用户Session 00:05:00 - 服务器断电(机房故障) 00:05:01 - Redis进程被杀,内存数据全部丢失 00:10:00 - 服务器恢复,Redis重启 00:10:01 - Redis启动成功,但数据为空! 问题: 1. 所有商品详情缓存丢失 → 10万次数据库查询 2. 所有用户Session丢失 → 50万用户被强制登出 3. 缓存预热需要10分钟 → 数据库压力巨大 4. 用户体验极差 → 大量用户流失 损失: - 用户流失:50万用户 × 10% = 5万用户 - 订单损失:5万用户 × 20% = 1万单 - GMV损失:1万单 × 200元 = 200万元 核心问题:Redis是内存数据库,断电后数据丢失,需要持久化。 1.2 持久化的本质 持久化:将内存中的数据保存到磁盘,重启后可以恢复。 为什么需要持久化? 原因1:数据安全 - Redis宕机、服务器断电、进程被杀 - 内存数据丢失 - 持久化后可以恢复 原因2:快速恢复 - 没有持久化:需要从数据库重新加载(10分钟+) - 有持久化:直接加载持久化文件(1分钟) 原因3:数据备份 - 定期备份持久化文件 - 灾难恢复(机房火灾、磁盘损坏) Redis的两种持久化方式: ...

2025-11-03 · maneng

Redis高可用架构:主从复制、哨兵、集群

一、引子:单机Redis的三大困境 假设你正在运行一个电商网站,使用单机Redis作为缓存。随着业务增长,你会遇到三个核心问题: 1.1 问题1:单点故障(可用性) 场景:双11大促,凌晨0点 00:00:00 - Redis单机宕机(内存不足OOM) 00:00:01 - 所有请求打到数据库 00:00:02 - 数据库连接池耗尽(1000个请求/秒) 00:00:03 - 数据库CPU 100% 00:00:05 - 网站503错误,用户无法下单 00:00:10 - 运维手动重启Redis 00:00:15 - Redis启动成功,但缓存为空 00:00:20 - 缓存预热中(需要10分钟) 00:10:00 - 系统恢复正常 损失: - 10分钟宕机时间 - 订单损失:10分钟 × 1000单/分钟 = 1万单 - GMV损失:1万单 × 200元/单 = 200万元 核心问题:单点故障导致系统不可用,无容灾能力。 可用性计算: 假设:Redis每月宕机1次,每次10分钟 可用性 = (30天 × 24小时 × 60分钟 - 10分钟) / (30天 × 24小时 × 60分钟) = (43200 - 10) / 43200 = 99.977% 看起来还不错?但实际上: - 1年12次宕机,累计120分钟 = 2小时 - 如果恰好在大促期间宕机,损失巨大 - 高可用系统要求:99.99%(年宕机时间 < 52分钟) 1.2 问题2:容量瓶颈(可扩展性) 场景:业务增长,数据量暴增 第1个月: - 商品数量:10万 - 缓存数据量:2GB - 单机Redis:4GB内存(够用) 第6个月: - 商品数量:100万 - 缓存数据量:20GB - 单机Redis:4GB内存(不够用!) 解决方案1:垂直扩展(加内存) - 4GB → 16GB(成本翻倍,但有上限) - 最大:512GB(成本极高,且有物理上限) 解决方案2:水平扩展(加机器) - 需要Redis集群(数据分片) 核心问题:单机内存有限,垂直扩展有上限,需要水平扩展。 ...

2025-11-03 · maneng

Redis实战:分布式锁、消息队列、缓存设计

一、分布式锁:从SETNX到Redlock 1.1 为什么需要分布式锁? 场景:秒杀系统的超卖问题 // ❌ 错误:单机锁无法解决分布式超卖 @Service public class SeckillService { @Autowired private ProductRepository productRepository; /** * 秒杀下单(单机锁) */ public synchronized Order seckill(Long productId, Long userId) { // 1. 检查库存 Integer stock = productRepository.getStock(productId); if (stock <= 0) { throw new SoldOutException("商品已售罄"); } // 2. 扣减库存 productRepository.decrementStock(productId); // 3. 创建订单 return orderService.createOrder(productId, userId); } } // 问题: // 假设有3台服务器(Server A、B、C) // T1: Server A:检查库存=1(通过) // T2: Server B:检查库存=1(通过) // T3: Server A:扣减库存=0,创建订单1 // T4: Server B:扣减库存=-1(超卖!),创建订单2 // synchronized只能锁住单机JVM内的线程 // 无法锁住分布式环境的多个进程 核心问题:分布式环境下,单机锁无效,需要分布式锁。 ...

2025-11-03 · maneng

渠道共享库存中心:Redis分布式锁的生产实践

引子:一次价值百万的库存超卖事故 2024年双十一,凌晨00:05分,我的手机突然响起刺耳的告警声。 打开监控大盘,一行红色数字让我瞬间清醒:某爆款SKU超卖217件。这意味着我们已经卖出了比实际库存多217件商品,客户投诉、赔偿成本、品牌信任危机接踵而至。 这不是我第一次遇到超卖问题,但这次的损失格外惨重——该SKU是限量联名款,成本价就超过500元,217件的赔偿成本加上品牌损失,直接损失超过百万。 事后复盘,我们发现问题的根源:在分布式环境下,单机锁完全失效了。 10个服务实例同时处理来自Amazon、Shopify、天猫国际等多个渠道的订单,每个实例内部的synchronized锁只能保证单个JVM内的线程安全,但对于跨JVM的并发请求,库存扣减的原子性荡然无存。 这次事故后,我们用了整整一周时间,重构了整个库存中心的并发控制机制,将Redis分布式锁引入生产环境。三个月后,超卖率从5%降至0.1%,系统TPS从200提升到2000。 这篇文章,就是那次重构的完整技术总结。 问题本质:分布式环境下的并发控制 业务场景 我们的渠道共享库存中心需要服务10+电商平台,这些平台的订单会同时扣减同一个商品的库存: 时间 渠道 SKU-1001 操作 当前库存 00:01 Amazon -1 扣减 100 → 99 00:01 Shopify -2 扣减 99 → 97 00:01 天猫国际 -1 扣减 97 → 96 00:01 独立站 -3 扣减 96 → 93 在分布式部署下(假设5个服务实例),这些请求可能被不同的实例处理。如果没有正确的并发控制机制,就会出现经典的竞态条件(Race Condition)。 错误方案的代价 我们尝试过的失败方案: ❌ 方案1:数据库行锁 SELECT * FROM inventory WHERE sku = 'SKU-1001' FOR UPDATE; UPDATE inventory SET quantity = quantity - 1 WHERE sku = 'SKU-1001'; 问题: 性能极差:TPS<50,远低于业务需求 锁等待严重:高并发时大量请求超时 数据库压力大:成为系统瓶颈 ❌ 方案2:乐观锁(版本号) // 基于版本号的乐观锁 UPDATE inventory SET quantity = quantity - 1, version = version + 1 WHERE sku = 'SKU-1001' AND version = 10; 问题: ...

2025-10-15 · maneng

渠道共享库存中心:Redis分布式锁的生产实践

引子:一次价值百万的库存超卖事故 2024年双十一,凌晨00:05分,我的手机突然响起刺耳的告警声。 打开监控大盘,一行红色数字让我瞬间清醒:某爆款SKU超卖217件。这意味着我们已经卖出了比实际库存多217件商品,客户投诉、赔偿成本、品牌信任危机接踵而至。 这不是我第一次遇到超卖问题,但这次的损失格外惨重——该SKU是限量联名款,成本价就超过500元,217件的赔偿成本加上品牌损失,直接损失超过百万。 事后复盘,我们发现问题的根源:在分布式环境下,单机锁完全失效了。 10个服务实例同时处理来自Amazon、Shopify、天猫国际等多个渠道的订单,每个实例内部的synchronized锁只能保证单个JVM内的线程安全,但对于跨JVM的并发请求,库存扣减的原子性荡然无存。 这次事故后,我们用了整整一周时间,重构了整个库存中心的并发控制机制,将Redis分布式锁引入生产环境。三个月后,超卖率从5%降至0.1%,系统TPS从200提升到2000。 这篇文章,就是那次重构的完整技术总结。 问题本质:分布式环境下的并发控制 业务场景 我们的渠道共享库存中心需要服务10+电商平台,这些平台的订单会同时扣减同一个商品的库存: 时间 渠道 SKU-1001 操作 当前库存 00:01 Amazon -1 扣减 100 → 99 00:01 Shopify -2 扣减 99 → 97 00:01 天猫国际 -1 扣减 97 → 96 00:01 独立站 -3 扣减 96 → 93 在分布式部署下(假设5个服务实例),这些请求可能被不同的实例处理。如果没有正确的并发控制机制,就会出现经典的竞态条件(Race Condition)。 错误方案的代价 我们尝试过的失败方案: ❌ 方案1:数据库行锁 SELECT * FROM inventory WHERE sku = 'SKU-1001' FOR UPDATE; UPDATE inventory SET quantity = quantity - 1 WHERE sku = 'SKU-1001'; 问题: 性能极差:TPS<50,远低于业务需求 锁等待严重:高并发时大量请求超时 数据库压力大:成为系统瓶颈 ❌ 方案2:乐观锁(版本号) // 基于版本号的乐观锁 UPDATE inventory SET quantity = quantity - 1, version = version + 1 WHERE sku = 'SKU-1001' AND version = 10; 问题: ...

2025-10-15 · maneng

渠道共享库存中心:Redis分布式锁的生产实践

引子:一次价值百万的库存超卖事故 2024年双十一,凌晨00:05分,我的手机突然响起刺耳的告警声。 打开监控大盘,一行红色数字让我瞬间清醒:某爆款SKU超卖217件。这意味着我们已经卖出了比实际库存多217件商品,客户投诉、赔偿成本、品牌信任危机接踵而至。 这不是我第一次遇到超卖问题,但这次的损失格外惨重——该SKU是限量联名款,成本价就超过500元,217件的赔偿成本加上品牌损失,直接损失超过百万。 事后复盘,我们发现问题的根源:在分布式环境下,单机锁完全失效了。 10个服务实例同时处理来自Amazon、Shopify、天猫国际等多个渠道的订单,每个实例内部的synchronized锁只能保证单个JVM内的线程安全,但对于跨JVM的并发请求,库存扣减的原子性荡然无存。 这次事故后,我们用了整整一周时间,重构了整个库存中心的并发控制机制,将Redis分布式锁引入生产环境。三个月后,超卖率从5%降至0.1%,系统TPS从200提升到2000。 这篇文章,就是那次重构的完整技术总结。 问题本质:分布式环境下的并发控制 业务场景 我们的渠道共享库存中心需要服务10+电商平台,这些平台的订单会同时扣减同一个商品的库存: 时间 渠道 SKU-1001 操作 当前库存 00:01 Amazon -1 扣减 100 → 99 00:01 Shopify -2 扣减 99 → 97 00:01 天猫国际 -1 扣减 97 → 96 00:01 独立站 -3 扣减 96 → 93 在分布式部署下(假设5个服务实例),这些请求可能被不同的实例处理。如果没有正确的并发控制机制,就会出现经典的竞态条件(Race Condition)。 错误方案的代价 我们尝试过的失败方案: ❌ 方案1:数据库行锁 SELECT * FROM inventory WHERE sku = 'SKU-1001' FOR UPDATE; UPDATE inventory SET quantity = quantity - 1 WHERE sku = 'SKU-1001'; 问题: 性能极差:TPS<50,远低于业务需求 锁等待严重:高并发时大量请求超时 数据库压力大:成为系统瓶颈 ❌ 方案2:乐观锁(版本号) // 基于版本号的乐观锁 UPDATE inventory SET quantity = quantity - 1, version = version + 1 WHERE sku = 'SKU-1001' AND version = 10; 问题: ...

2025-10-15 · maneng

故障排查手册:快速定位和解决Redis问题

故障分类 故障类型 典型现象 排查优先级 连接问题 无法连接、超时 🔴 最高 性能问题 响应慢、卡顿 🟡 高 内存问题 OOM、淘汰 🟡 高 数据问题 数据丢失、不一致 🟡 高 集群问题 节点下线、脑裂 🔴 最高 1. 连接问题排查 问题1:无法连接Redis 症状: Connection refused Could not connect to Redis at 127.0.0.1:6379 排查步骤: # 1. 检查Redis进程 ps aux | grep redis systemctl status redis # 2. 检查端口监听 netstat -tulnp | grep 6379 ss -tulnp | grep 6379 # 3. 检查防火墙 iptables -L | grep 6379 firewall-cmd --list-all # 4. 检查绑定地址 redis-cli CONFIG GET bind # 如果是127.0.0.1,外部无法访问 # 5. 测试连接 redis-cli -h 127.0.0.1 -p 6379 PING telnet 127.0.0.1 6379 解决方案: ...

2025-01-22 · maneng

监控告警体系:构建Redis可观测性

核心监控指标 1. 性能指标 @Component public class RedisMetricsCollector { @Autowired private RedisTemplate<String, String> redis; // QPS(每秒查询数) public long getQPS() { Properties info = redis.execute((RedisCallback<Properties>) connection -> connection.info("stats") ); return Long.parseLong(info.getProperty("instantaneous_ops_per_sec")); } // 延迟 public long getLatency() { long start = System.currentTimeMillis(); redis.opsForValue().get("health_check"); return System.currentTimeMillis() - start; } // 命中率 public double getHitRate() { Properties info = redis.execute((RedisCallback<Properties>) connection -> connection.info("stats") ); long hits = Long.parseLong(info.getProperty("keyspace_hits")); long misses = Long.parseLong(info.getProperty("keyspace_misses")); if (hits + misses == 0) { return 0; } return hits * 100.0 / (hits + misses); } // 慢查询数量 public long getSlowlogCount() { return redis.execute((RedisCallback<Long>) connection -> connection.slowlogLen() ); } } 2. 资源指标 // 内存使用 public Map<String, Object> getMemoryMetrics() { Properties info = redis.execute((RedisCallback<Properties>) connection -> connection.info("memory") ); Map<String, Object> metrics = new HashMap<>(); metrics.put("used_memory", Long.parseLong(info.getProperty("used_memory"))); metrics.put("used_memory_rss", Long.parseLong(info.getProperty("used_memory_rss"))); metrics.put("mem_fragmentation_ratio", Double.parseDouble(info.getProperty("mem_fragmentation_ratio"))); metrics.put("evicted_keys", Long.parseLong(info.getProperty("evicted_keys"))); return metrics; } // CPU使用 public double getCPUUsage() { Properties info = redis.execute((RedisCallback<Properties>) connection -> connection.info("cpu") ); return Double.parseDouble(info.getProperty("used_cpu_sys")); } // 连接数 public Map<String, Long> getConnectionMetrics() { Properties info = redis.execute((RedisCallback<Properties>) connection -> connection.info("clients") ); Map<String, Long> metrics = new HashMap<>(); metrics.put("connected_clients", Long.parseLong(info.getProperty("connected_clients"))); metrics.put("blocked_clients", Long.parseLong(info.getProperty("blocked_clients"))); return metrics; } 3. 持久化指标 public Map<String, Object> getPersistenceMetrics() { Properties info = redis.execute((RedisCallback<Properties>) connection -> connection.info("persistence") ); Map<String, Object> metrics = new HashMap<>(); // RDB metrics.put("rdb_last_save_time", Long.parseLong(info.getProperty("rdb_last_save_time"))); metrics.put("rdb_changes_since_last_save", Long.parseLong(info.getProperty("rdb_changes_since_last_save"))); // AOF if ("1".equals(info.getProperty("aof_enabled"))) { metrics.put("aof_current_size", Long.parseLong(info.getProperty("aof_current_size"))); metrics.put("aof_base_size", Long.parseLong(info.getProperty("aof_base_size"))); } return metrics; } 4. 复制指标 public Map<String, Object> getReplicationMetrics() { Properties info = redis.execute((RedisCallback<Properties>) connection -> connection.info("replication") ); Map<String, Object> metrics = new HashMap<>(); metrics.put("role", info.getProperty("role")); if ("master".equals(info.getProperty("role"))) { metrics.put("connected_slaves", Integer.parseInt(info.getProperty("connected_slaves"))); } else { metrics.put("master_link_status", info.getProperty("master_link_status")); metrics.put("master_last_io_seconds_ago", Integer.parseInt(info.getProperty("master_last_io_seconds_ago"))); } return metrics; } 告警规则 1. 性能告警 @Component public class PerformanceAlerting { @Autowired private RedisMetricsCollector metrics; @Scheduled(fixedRate = 60000) // 每分钟 public void checkPerformance() { // QPS过高 long qps = metrics.getQPS(); if (qps > 50000) { sendAlert("QPS告警", String.format("当前QPS: %d", qps)); } // 延迟过高 long latency = metrics.getLatency(); if (latency > 100) { sendAlert("延迟告警", String.format("当前延迟: %dms", latency)); } // 命中率过低 double hitRate = metrics.getHitRate(); if (hitRate < 80) { sendAlert("命中率告警", String.format("当前命中率: %.2f%%", hitRate)); } // 慢查询过多 long slowlogCount = metrics.getSlowlogCount(); if (slowlogCount > 100) { sendAlert("慢查询告警", String.format("慢查询数量: %d", slowlogCount)); } } private void sendAlert(String title, String message) { log.warn("{}: {}", title, message); // 发送钉钉/邮件/短信告警 } } 2. 资源告警 @Scheduled(fixedRate = 60000) public void checkResources() { // 内存使用 Map<String, Object> memMetrics = metrics.getMemoryMetrics(); long usedMemory = (long) memMetrics.get("used_memory"); long maxMemory = 4L * 1024 * 1024 * 1024; // 4GB if (usedMemory > maxMemory * 0.9) { sendAlert("内存告警", String.format("内存使用: %dMB / %dMB", usedMemory / 1024 / 1024, maxMemory / 1024 / 1024)); } // 内存碎片率 double fragRatio = (double) memMetrics.get("mem_fragmentation_ratio"); if (fragRatio > 1.5) { sendAlert("内存碎片告警", String.format("碎片率: %.2f", fragRatio)); } // 连接数 Map<String, Long> connMetrics = metrics.getConnectionMetrics(); long connectedClients = connMetrics.get("connected_clients"); if (connectedClients > 1000) { sendAlert("连接数告警", String.format("当前连接数: %d", connectedClients)); } } 3. 可用性告警 @Scheduled(fixedRate = 10000) // 每10秒 public void checkAvailability() { try { // 健康检查 redis.opsForValue().get("health_check"); } catch (Exception e) { sendAlert("Redis不可用", e.getMessage()); } // 主从复制状态 Map<String, Object> replMetrics = metrics.getReplicationMetrics(); if ("slave".equals(replMetrics.get("role"))) { String linkStatus = (String) replMetrics.get("master_link_status"); if (!"up".equals(linkStatus)) { sendAlert("主从复制断开", "master_link_status: " + linkStatus); } int lastIO = (int) replMetrics.get("master_last_io_seconds_ago"); if (lastIO > 60) { sendAlert("主从复制延迟", String.format("最后同步时间: %d秒前", lastIO)); } } } Prometheus + Grafana监控 1. 安装Redis Exporter docker run -d \ --name redis-exporter \ -p 9121:9121 \ oliver006/redis_exporter:latest \ --redis.addr=redis://redis:6379 2. Prometheus配置 # prometheus.yml scrape_configs: - job_name: 'redis' static_configs: - targets: ['redis-exporter:9121'] 3. Grafana Dashboard 导入官方Dashboard: ...

2025-01-22 · maneng

内存优化实战:降低Redis内存占用

内存分析 查看内存使用 # 内存统计 INFO memory # 关键指标 used_memory: 1073741824 # 已使用内存(字节) used_memory_human: 1.00G used_memory_peak: 2147483648 maxmemory: 4294967296 # 最大内存限制 # 内存碎片率 mem_fragmentation_ratio: 1.20 # >1.5需要优化 # 查看key占用内存 MEMORY USAGE key Java内存分析 @Component public class MemoryAnalyzer { @Autowired private RedisTemplate<String, String> redis; public void analyze() { Properties info = redis.execute((RedisCallback<Properties>) connection -> connection.info("memory") ); long usedMemory = Long.parseLong(info.getProperty("used_memory")); long maxMemory = Long.parseLong(info.getProperty("maxmemory")); double fragRatio = Double.parseDouble(info.getProperty("mem_fragmentation_ratio")); log.info("内存使用: {}MB / {}MB ({}%)", usedMemory / 1024 / 1024, maxMemory / 1024 / 1024, usedMemory * 100 / maxMemory); log.info("碎片率: {}", fragRatio); if (usedMemory > maxMemory * 0.9) { log.warn("内存使用超过90%,需要优化"); } if (fragRatio > 1.5) { log.warn("内存碎片率过高,考虑重启Redis"); } } // 分析TOP 100大key public List<KeyMemory> analyzeTopKeys() { List<KeyMemory> result = new ArrayList<>(); ScanOptions options = ScanOptions.scanOptions().count(100).build(); redis.execute((RedisCallback<Object>) connection -> { Cursor<byte[]> cursor = connection.scan(options); while (cursor.hasNext()) { String key = new String(cursor.next()); Long size = connection.memoryUsage(key.getBytes()); if (size != null && size > 10240) { // > 10KB KeyMemory km = new KeyMemory(); km.setKey(key); km.setSize(size); result.add(km); } } return null; }); return result.stream() .sorted(Comparator.comparingLong(KeyMemory::getSize).reversed()) .limit(100) .collect(Collectors.toList()); } } 优化策略 1. 选择合适的数据类型 // ❌ 不好:String存储对象(JSON) redis.opsForValue().set("user:1001", "{\"name\":\"Alice\",\"age\":25,\"city\":\"Beijing\"}"); // 占用:~100字节 // ✅ 好:Hash存储(小对象) redis.opsForHash().put("user:1001", "name", "Alice"); redis.opsForHash().put("user:1001", "age", "25"); redis.opsForHash().put("user:1001", "city", "Beijing"); // 占用:~50字节,节省50% 2. 利用紧凑编码 // Hash:保持ziplist编码 // 条件:字段数<512,值长度<64字节 public void setUserInfo(Long userId, Map<String, String> info) { String key = "user:" + userId; // 确保字段值简短 Map<String, String> compactInfo = info.entrySet().stream() .filter(e -> e.getValue().length() < 64) // 值<64字节 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); redis.opsForHash().putAll(key, compactInfo); // 验证编码 String encoding = redis.execute((RedisCallback<String>) connection -> new String(connection.execute("OBJECT", "ENCODING".getBytes(), key.getBytes())) ); log.info("编码: {}", encoding); // 期望:ziplist或listpack } // 配置优化 // redis.conf hash-max-ziplist-entries 512 hash-max-ziplist-value 64 3. 整数优化 // ❌ 不好:字符串存储整数 redis.opsForValue().set("count", "12345"); // embstr编码,~20字节 // ✅ 好:整数存储 redis.opsForValue().set("count", 12345); // int编码,8字节 // ✅ 更好:共享整数对象(0-9999) redis.opsForValue().set("status", 1); // 共享对象,0字节(不计redisObject) 4. 压缩value @Service public class CompressedCacheService { @Autowired private RedisTemplate<String, byte[]> redis; // 存储大value时压缩 public void setLarge(String key, String value) throws IOException { byte[] data = value.getBytes(StandardCharsets.UTF_8); if (data.length > 1024) { // >1KB才压缩 // Gzip压缩 ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) { gzip.write(data); } byte[] compressed = bos.toByteArray(); // 添加压缩标记 byte[] withFlag = new byte[compressed.length + 1]; withFlag[0] = 1; // 压缩标记 System.arraycopy(compressed, 0, withFlag, 1, compressed.length); redis.opsForValue().set(key, withFlag); log.info("压缩率: {}%", (1 - compressed.length * 1.0 / data.length) * 100); } else { // 不压缩 byte[] withFlag = new byte[data.length + 1]; withFlag[0] = 0; // 未压缩标记 System.arraycopy(data, 0, withFlag, 1, data.length); redis.opsForValue().set(key, withFlag); } } public String getLarge(String key) throws IOException { byte[] data = redis.opsForValue().get(key); if (data == null || data.length == 0) { return null; } byte flag = data[0]; byte[] content = new byte[data.length - 1]; System.arraycopy(data, 1, content, 0, content.length); if (flag == 1) { // 解压 ByteArrayInputStream bis = new ByteArrayInputStream(content); ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (GZIPInputStream gzip = new GZIPInputStream(bis)) { byte[] buffer = new byte[1024]; int len; while ((len = gzip.read(buffer)) > 0) { bos.write(buffer, 0, len); } } return new String(bos.toByteArray(), StandardCharsets.UTF_8); } else { return new String(content, StandardCharsets.UTF_8); } } } 5. 设置合理的过期时间 // ❌ 不好:永不过期 redis.opsForValue().set("cache:data", data); // ✅ 好:设置过期时间 redis.opsForValue().set("cache:data", data, 3600, TimeUnit.SECONDS); // ✅ 更好:随机过期时间(防止雪崩) int expire = 3600 + ThreadLocalRandom.current().nextInt(300); redis.opsForValue().set("cache:data", data, expire, TimeUnit.SECONDS); 6. 定期清理过期数据 @Component public class CacheCleanup { @Autowired private RedisTemplate<String, String> redis; @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点 public void cleanup() { // 清理超过30天的时间线数据 long threshold = System.currentTimeMillis() - 30L * 24 * 3600 * 1000; Long removed = redis.opsForZSet().removeRangeByScore("timeline", 0, threshold); log.info("清理过期时间线数据: {} 条", removed); // 清理空Hash Set<String> hashKeys = redis.keys("user:*"); if (hashKeys != null) { for (String key : hashKeys) { Long size = redis.opsForHash().size(key); if (size != null && size == 0) { redis.delete(key); log.info("删除空Hash: {}", key); } } } } } 7. 拆分BigKey // ❌ 不好:单个大Hash HSET user:1001 field1 val1 ... field10000 val10000 // 1MB // ✅ 好:分片存储 @Service public class ShardedHashService { private static final int SHARD_COUNT = 10; public void hset(String key, String field, String value) { int shard = Math.abs(field.hashCode()) % SHARD_COUNT; String shardKey = key + ":shard:" + shard; redis.opsForHash().put(shardKey, field, value); } public String hget(String key, String field) { int shard = Math.abs(field.hashCode()) % SHARD_COUNT; String shardKey = key + ":shard:" + shard; return (String) redis.opsForHash().get(shardKey, field); } } 内存淘汰策略 # redis.conf # 最大内存 maxmemory 4gb # 淘汰策略 maxmemory-policy allkeys-lru # 策略说明: # noeviction: 不淘汰,内存满时写入报错 # allkeys-lru: 所有key,LRU淘汰 # volatile-lru: 有过期时间的key,LRU淘汰 # allkeys-lfu: 所有key,LFU淘汰(访问频率) # volatile-lfu: 有过期时间的key,LFU淘汰 # allkeys-random: 所有key,随机淘汰 # volatile-random: 有过期时间的key,随机淘汰 # volatile-ttl: 有过期时间的key,TTL小的先淘汰 # LRU采样数量(越大越精确,但消耗CPU) maxmemory-samples 5 监控告警 @Component public class MemoryMonitor { @Autowired private RedisTemplate<String, String> redis; @Scheduled(fixedRate = 300000) // 每5分钟 public void monitor() { Properties info = redis.execute((RedisCallback<Properties>) connection -> connection.info("memory") ); long usedMemory = Long.parseLong(info.getProperty("used_memory")); long maxMemory = Long.parseLong(info.getProperty("maxmemory")); long evicted = Long.parseLong(info.getProperty("evicted_keys")); double usage = usedMemory * 100.0 / maxMemory; log.info("内存使用: {}%", String.format("%.2f", usage)); // 告警:内存使用超过80% if (usage > 80) { log.warn("Redis内存使用超过80%,需要扩容或优化"); sendAlert("Redis内存告警", String.format("当前使用%.2f%%", usage)); } // 告警:淘汰key过多 if (evicted > 1000) { log.warn("Redis淘汰key过多: {}", evicted); sendAlert("Redis淘汰告警", String.format("已淘汰%d个key", evicted)); } } private void sendAlert(String title, String message) { // 发送告警 } } 内存优化清单 数据结构: ...

2025-01-21 · maneng

慢查询优化:发现与解决性能瓶颈

慢查询配置 # 慢查询阈值(微秒),默认10000(10ms) CONFIG SET slowlog-log-slower-than 10000 # 慢查询日志最大长度 CONFIG SET slowlog-max-len 128 # 持久化配置 # redis.conf slowlog-log-slower-than 10000 slowlog-max-len 128 查看慢查询 # 获取所有慢查询 SLOWLOG GET [count] # 获取慢查询数量 SLOWLOG LEN # 清空慢查询 SLOWLOG RESET 输出示例: redis> SLOWLOG GET 5 1) 1) (integer) 6 # 日志ID 2) (integer) 1609459200 # 时间戳 3) (integer) 12000 # 执行时间(微秒) 4) 1) "KEYS" # 命令 2) "*" 5) "127.0.0.1:50796" # 客户端 6) "" # 客户端名称 Java监控慢查询 @Component public class SlowQueryMonitor { @Autowired private RedisTemplate<String, String> redis; @Scheduled(fixedRate = 60000) // 每分钟 public void checkSlowQueries() { List<Object> slowlogs = redis.execute((RedisCallback<List<Object>>) connection -> connection.slowlogGet(100) ); if (slowlogs != null && !slowlogs.isEmpty()) { for (Object log : slowlogs) { // 解析慢查询日志 Map<String, Object> slowlog = parseSlowlog(log); long duration = (long) slowlog.get("duration"); String command = (String) slowlog.get("command"); // 告警阈值:超过50ms if (duration > 50000) { log.warn("慢查询告警: command={}, duration={}ms", command, duration / 1000); // 发送告警 sendAlert(command, duration); } } // 清空已分析的慢查询 redis.execute((RedisCallback<Void>) connection -> { connection.slowlogReset(); return null; }); } } private Map<String, Object> parseSlowlog(Object log) { // 解析慢查询日志格式 return new HashMap<>(); } private void sendAlert(String command, long duration) { // 发送告警(邮件/短信/钉钉) } } 常见慢命令 1. KEYS命令 # ❌ 极慢:遍历所有key,O(N) KEYS * KEYS user:* # ✅ 好:使用SCAN,渐进式遍历 SCAN 0 MATCH user:* COUNT 100 Java替代方案: ...

2025-01-21 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...