Redis第一性原理:为什么我们需要缓存?

一、引子:商品详情接口的性能进化之路 想象你正在开发一个电商平台的商品详情页面,每次用户访问都需要查询商品基本信息、品牌信息、类目信息、库存信息和商品图片。让我们看看三种不同的实现方式,以及它们各自的性能表现。 1.1 场景A:无缓存(直接查数据库) 这是最直接的实现方式:每次请求都查询数据库。 @Service public class ProductService { @Autowired private ProductRepository productRepository; @Autowired private BrandRepository brandRepository; @Autowired private CategoryRepository categoryRepository; @Autowired private InventoryRepository inventoryRepository; @Autowired private ProductImageRepository productImageRepository; /** * 查询商品详情(每次请求都查数据库) * 平均耗时:100ms * QPS上限:500 */ public ProductDetailVO getProductDetail(Long productId) { // 1. 查询商品基本信息(20ms) Product product = productRepository.findById(productId); if (product == null) { throw new ProductNotFoundException("商品不存在:" + productId); } // 2. 查询品牌信息(20ms) Brand brand = brandRepository.findById(product.getBrandId()); // 3. 查询类目信息(20ms) Category category = categoryRepository.findById(product.getCategoryId()); // 4. 查询库存信息(20ms) Inventory inventory = inventoryRepository.findByProductId(productId); // 5. 查询商品图片(20ms,可能有N+1查询问题) List<ProductImage> images = productImageRepository.findByProductId(productId); // 6. 组装返回对象 ProductDetailVO vo = new ProductDetailVO(); vo.setProductId(product.getId()); vo.setProductName(product.getName()); vo.setPrice(product.getPrice()); vo.setBrandName(brand.getName()); vo.setCategoryName(category.getName()); vo.setStock(inventory.getStock()); vo.setImages(images); return vo; } } 性能数据(压测工具:JMeter,1000并发): ...

2025-11-03 · maneng

MySQL索引原理:从O(n)到O(log n)的飞跃

引言 “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。” —— David Wheeler 在上一篇文章《MySQL第一性原理:为什么我们需要数据库?》中,我们了解到MySQL通过索引将查询性能从O(n)提升到O(log n),实现了10万倍的性能飞跃。 但你有没有想过: 为什么索引能这么快? 从1亿条数据中查找一条,只需要3-4次磁盘IO 为什么MySQL选择B+树? 而不是哈希表、二叉搜索树、红黑树? 为什么有些查询用不上索引? WHERE id + 1 = 100 vs WHERE id = 99 联合索引的最左前缀原则是什么? 为什么(user_id, created_time)索引无法优化WHERE created_time > xxx? 今天,我们从第一性原理出发,通过对比5种数据结构,深度剖析MySQL索引的演进历程: 顺序扫描 → 哈希索引 → 二叉搜索树 → B树 → B+树 O(n) O(1) O(log₂n) O(logₘn) O(logₘn) + 顺序IO 1秒 ? ? ? 10微秒 我们还将手写300行代码,实现一个简化版B+树,彻底理解索引的实现原理。 一、问题的起点:全表扫描的性能瓶颈 让我们从一个最简单的查询开始: -- 用户表,1亿条数据 SELECT * FROM users WHERE id = 50000000; 如果没有索引,MySQL会怎么做? 1.1 顺序扫描(Full Table Scan) 执行过程: 1. 从磁盘读取第1页数据(16KB,约100行)→ 判断id是否等于50000000 → 不是 2. 从磁盘读取第2页数据 → 判断 → 不是 3. 从磁盘读取第3页数据 → 判断 → 不是 ... 500000. 从磁盘读取第50万页数据 → 判断 → 是! ✅ 总计:扫描50万页,约8GB数据 性能分析: ...

2025-11-03 · maneng

MySQL索引原理:从O(n)到O(log n)的飞跃

引言 “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。” —— David Wheeler 在上一篇文章《MySQL第一性原理:为什么我们需要数据库?》中,我们了解到MySQL通过索引将查询性能从O(n)提升到O(log n),实现了10万倍的性能飞跃。 但你有没有想过: 为什么索引能这么快? 从1亿条数据中查找一条,只需要3-4次磁盘IO 为什么MySQL选择B+树? 而不是哈希表、二叉搜索树、红黑树? 为什么有些查询用不上索引? WHERE id + 1 = 100 vs WHERE id = 99 联合索引的最左前缀原则是什么? 为什么(user_id, created_time)索引无法优化WHERE created_time > xxx? 今天,我们从第一性原理出发,通过对比5种数据结构,深度剖析MySQL索引的演进历程: 顺序扫描 → 哈希索引 → 二叉搜索树 → B树 → B+树 O(n) O(1) O(log₂n) O(logₘn) O(logₘn) + 顺序IO 1秒 ? ? ? 10微秒 我们还将手写300行代码,实现一个简化版B+树,彻底理解索引的实现原理。 一、问题的起点:全表扫描的性能瓶颈 让我们从一个最简单的查询开始: -- 用户表,1亿条数据 SELECT * FROM users WHERE id = 50000000; 如果没有索引,MySQL会怎么做? 1.1 顺序扫描(Full Table Scan) 执行过程: 1. 从磁盘读取第1页数据(16KB,约100行)→ 判断id是否等于50000000 → 不是 2. 从磁盘读取第2页数据 → 判断 → 不是 3. 从磁盘读取第3页数据 → 判断 → 不是 ... 500000. 从磁盘读取第50万页数据 → 判断 → 是! ✅ 总计:扫描50万页,约8GB数据 性能分析: ...

2025-11-03 · maneng

MySQL核心原理:关键技术深度解析

引言 通过前5篇文章,我们已经掌握了MySQL的核心技术。本文将深入MySQL内部,理解关键组件的工作原理。 一、InnoDB核心组件 1.1 Buffer Pool:内存缓存池 作用:缓存数据页和索引页,减少磁盘IO Buffer Pool结构: ┌─────────────────────────────────────┐ │ Buffer Pool (默认128MB) │ ├─────────────────────────────────────┤ │ 数据页缓存 (75%) │ │ ├─ Page 1: users表数据 │ │ ├─ Page 2: orders表数据 │ │ └─ ... │ ├─────────────────────────────────────┤ │ 索引页缓存 (20%) │ │ ├─ Index Page 1: uk_username │ │ ├─ Index Page 2: idx_created_time │ │ └─ ... │ ├─────────────────────────────────────┤ │ Undo页缓存 (5%) │ │ └─ 历史版本数据 │ └─────────────────────────────────────┘ 淘汰算法: LRU (Least Recently Used) 热数据: 靠近链表头部 冷数据: 靠近链表尾部 新数据: 先放入中间位置(避免扫描污染) 关键参数: ...

2025-11-03 · maneng

MySQL核心原理:关键技术深度解析

引言 通过前5篇文章,我们已经掌握了MySQL的核心技术。本文将深入MySQL内部,理解关键组件的工作原理。 一、InnoDB核心组件 1.1 Buffer Pool:内存缓存池 作用:缓存数据页和索引页,减少磁盘IO Buffer Pool结构: ┌─────────────────────────────────────┐ │ Buffer Pool (默认128MB) │ ├─────────────────────────────────────┤ │ 数据页缓存 (75%) │ │ ├─ Page 1: users表数据 │ │ ├─ Page 2: orders表数据 │ │ └─ ... │ ├─────────────────────────────────────┤ │ 索引页缓存 (20%) │ │ ├─ Index Page 1: uk_username │ │ ├─ Index Page 2: idx_created_time │ │ └─ ... │ ├─────────────────────────────────────┤ │ Undo页缓存 (5%) │ │ └─ 历史版本数据 │ └─────────────────────────────────────┘ 淘汰算法: LRU (Least Recently Used) 热数据: 靠近链表头部 冷数据: 靠近链表尾部 新数据: 先放入中间位置(避免扫描污染) 关键参数: ...

2025-11-03 · maneng

MySQL核心原理:关键技术深度解析

引言 通过前5篇文章,我们已经掌握了MySQL的核心技术。本文将深入MySQL内部,理解关键组件的工作原理。 一、InnoDB核心组件 1.1 Buffer Pool:内存缓存池 作用:缓存数据页和索引页,减少磁盘IO Buffer Pool结构: ┌─────────────────────────────────────┐ │ Buffer Pool (默认128MB) │ ├─────────────────────────────────────┤ │ 数据页缓存 (75%) │ │ ├─ Page 1: users表数据 │ │ ├─ Page 2: orders表数据 │ │ └─ ... │ ├─────────────────────────────────────┤ │ 索引页缓存 (20%) │ │ ├─ Index Page 1: uk_username │ │ ├─ Index Page 2: idx_created_time │ │ └─ ... │ ├─────────────────────────────────────┤ │ Undo页缓存 (5%) │ │ └─ 历史版本数据 │ └─────────────────────────────────────┘ 淘汰算法: LRU (Least Recently Used) 热数据: 靠近链表头部 冷数据: 靠近链表尾部 新数据: 先放入中间位置(避免扫描污染) 关键参数: ...

2025-11-03 · maneng

Redis第一性原理:为什么我们需要缓存?

一、引子:商品详情接口的性能进化之路 想象你正在开发一个电商平台的商品详情页面,每次用户访问都需要查询商品基本信息、品牌信息、类目信息、库存信息和商品图片。让我们看看三种不同的实现方式,以及它们各自的性能表现。 1.1 场景A:无缓存(直接查数据库) 这是最直接的实现方式:每次请求都查询数据库。 @Service public class ProductService { @Autowired private ProductRepository productRepository; @Autowired private BrandRepository brandRepository; @Autowired private CategoryRepository categoryRepository; @Autowired private InventoryRepository inventoryRepository; @Autowired private ProductImageRepository productImageRepository; /** * 查询商品详情(每次请求都查数据库) * 平均耗时:100ms * QPS上限:500 */ public ProductDetailVO getProductDetail(Long productId) { // 1. 查询商品基本信息(20ms) Product product = productRepository.findById(productId); if (product == null) { throw new ProductNotFoundException("商品不存在:" + productId); } // 2. 查询品牌信息(20ms) Brand brand = brandRepository.findById(product.getBrandId()); // 3. 查询类目信息(20ms) Category category = categoryRepository.findById(product.getCategoryId()); // 4. 查询库存信息(20ms) Inventory inventory = inventoryRepository.findByProductId(productId); // 5. 查询商品图片(20ms,可能有N+1查询问题) List<ProductImage> images = productImageRepository.findByProductId(productId); // 6. 组装返回对象 ProductDetailVO vo = new ProductDetailVO(); vo.setProductId(product.getId()); vo.setProductName(product.getName()); vo.setPrice(product.getPrice()); vo.setBrandName(brand.getName()); vo.setCategoryName(category.getName()); vo.setStock(inventory.getStock()); vo.setImages(images); return vo; } } 性能数据(压测工具:JMeter,1000并发): ...

2025-11-03 · 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

热Key与BigKey:发现、分析与解决方案

热Key问题 定义:访问频率极高的key,导致单个Redis节点负载过高 危害: 单节点CPU 100% 网络带宽打满 影响其他key访问 Cluster集群数据倾斜 发现热Key 方法1:redis-cli –hotkeys redis-cli --hotkeys # 统计访问频率最高的key 方法2:monitor命令 redis-cli monitor | head -n 100000 | awk '{print $4}' | sort | uniq -c | sort -rn | head -n 10 方法3:代码统计 @Aspect @Component public class RedisMonitorAspect { private ConcurrentHashMap<String, AtomicLong> accessCounter = new ConcurrentHashMap<>(); @Around("execution(* org.springframework.data.redis.core.RedisTemplate.opsFor*(..))") public Object around(ProceedingJoinPoint pjp) throws Throwable { Object result = pjp.proceed(); // 统计key访问次数 if (result != null) { String key = extractKey(pjp); if (key != null) { accessCounter.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet(); } } return result; } @Scheduled(fixedRate = 60000) // 每分钟 public void reportHotKeys() { List<Map.Entry<String, AtomicLong>> hotKeys = accessCounter.entrySet().stream() .sorted(Map.Entry.<String, AtomicLong>comparingByValue().reversed()) .limit(10) .collect(Collectors.toList()); log.info("热Key TOP 10: {}", hotKeys); // 清空统计 accessCounter.clear(); } } 解决方案 方案1:本地缓存 @Service public class ProductService { @Autowired private RedisTemplate<String, Object> redis; // 本地缓存热点数据 private Cache<String, Product> localCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(5, TimeUnit.MINUTES) .build(); public Product getProduct(Long productId) { String key = "product:" + productId; // 1. 本地缓存 Product product = localCache.getIfPresent(key); if (product != null) { return product; } // 2. Redis product = (Product) redis.opsForValue().get(key); if (product != null) { localCache.put(key, product); return product; } // 3. DB product = productMapper.selectById(productId); if (product != null) { redis.opsForValue().set(key, product, 3600, TimeUnit.SECONDS); localCache.put(key, product); } return product; } } 方案2:热Key备份 // 热Key复制多份,随机访问 public Object getHotKey(String key) { // 随机选择一个备份 int index = ThreadLocalRandom.current().nextInt(10); String backupKey = key + ":backup:" + index; Object value = redis.opsForValue().get(backupKey); if (value != null) { return value; } // 备份不存在,查询原key value = redis.opsForValue().get(key); if (value != null) { // 更新备份 redis.opsForValue().set(backupKey, value, 3600, TimeUnit.SECONDS); } return value; } 方案3:读写分离 // 读从节点,写主节点 @Configuration public class RedisConfig { @Bean public LettuceConnectionFactory writeConnectionFactory() { return new LettuceConnectionFactory(new RedisStandaloneConfiguration("master", 6379)); } @Bean public LettuceConnectionFactory readConnectionFactory() { return new LettuceConnectionFactory(new RedisStandaloneConfiguration("slave", 6379)); } } BigKey问题 定义:占用内存过大的key ...

2025-01-21 · maneng

如约数科科技工作室

浙ICP备2025203501号

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