架构思维:优雅解决缓存三大难题——穿透、击穿与雪崩

文章目录

引言

在高并发的应用系统中,缓存是提升系统性能、减轻数据库压力的关键技术。然而,缓存并非万能药,如果使用不当,反而会带来严重的系统风险。缓存穿透、缓存击穿和缓存雪崩这三大问题,是每个开发者在设计缓存系统时必须认真考虑的挑战。

一、缓存穿透:恶意请求直击数据库

问题本质

缓存穿透是指业务请求穿过了缓存层,直接落到持久化存储上。当大量请求访问不存在的数据时,由于缓存中没有对应数据,这些请求会直接打到数据库,可能导致数据库压力剧增甚至崩溃。

典型场景

  • 恶意攻击者利用不存在的ID发起大量请求
  • 系统存在大量"垃圾数据"查询
  • 缓存与数据库数据不一致

实战解决

客户端 布隆过滤器 缓存层 (Redis) 数据库 请求 Key: user_666 检查 Key 是否可能存在 不存在,直接拦截 请求 Key: user_123 Key 可能存在 查询 Key: user_123 未命中 (null) 查询数据库 数据不存在 写入空对象 (TTL: 5min) 防止后续穿透 返回 null 再次请求 Key: user_123 查询 Key: user_123 命中空对象 返回 null (来自缓存) 客户端 布隆过滤器 缓存层 (Redis) 数据库

方案1:缓存空对象(Null Value Caching)

java 复制代码
@Service
public class ProductService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    // 缓存空对象的过期时间(短于正常数据)
    private static final long NULL_CACHE_EXPIRE_SECONDS = 60;
    // 防止缓存穿透的特殊标记
    private static final String NULL_VALUE = "NULL";
    
    public Product getProductById(String productId) {
        // 1. 先从缓存中获取
        String cacheKey = "product:" + productId;
        Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
        
        // 2. 缓存存在且不是空标记,直接返回
        if (cacheValue != null) {
            if (NULL_VALUE.equals(cacheValue)) {
                return null; // 返回空对象
            }
            return (Product) cacheValue;
        }
        
        // 3. 缓存不存在,查询数据库
        Product product = productRepository.findById(productId).orElse(null);
        
        // 4. 数据库存在,缓存结果
        if (product != null) {
            redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
            return product;
        }
        
        // 5. 数据库不存在,缓存空对象
        redisTemplate.opsForValue().set(cacheKey, NULL_VALUE, NULL_CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
        return null;
    }
}

关键点

  • 使用特殊标记(如"NULL")表示空值,避免与正常数据混淆
  • 空对象的过期时间应短于正常数据,防止数据长期不一致
  • 注意内存占用问题,避免缓存大量空值

方案2:布隆过滤器(Bloom Filter)

布隆过滤器是一种空间效率极高的概率型数据结构,可以判断一个元素一定不存在可能存在

java 复制代码
@Configuration
public class BloomFilterConfig {
    
    // 假设系统中商品总数约为100万
    private static final int EXPECTED_INSERTIONS = 1_000_000;
    // 误判率控制在3%
    private static final double FALSE_POSITIVE_RATE = 0.03;
    
    @Bean
    public BloomFilter<String> productBloomFilter() {
        return BloomFilter.create(
            Funnels.stringFunnel(Charsets.UTF_8),
            EXPECTED_INSERTIONS,
            FALSE_POSITIVE_RATE
        );
    }
    
    @Bean
    public CommandLineRunner bloomFilterInitializer(
            BloomFilter<String> bloomFilter,
            ProductRepository productRepository) {
        return args -> {
            // 系统启动时,将所有商品ID加载到布隆过滤器
            productRepository.findAllIds().forEach(id -> bloomFilter.put(id));
            System.out.println("布隆过滤器初始化完成,已加载" + EXPECTED_INSERTIONS + "个商品ID");
        };
    }
}
java 复制代码
@Service
public class ProductService {
    
    @Autowired
    private BloomFilter<String> productBloomFilter;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    public Product getProductById(String productId) {
        // 1. 先通过布隆过滤器判断
        if (!productBloomFilter.mightContain(productId)) {
            // 肯定不存在,直接返回null
            return null;
        }
        
        // 2. 布隆过滤器说可能存在,继续检查缓存
        String cacheKey = "product:" + productId;
        Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
        
        if (cacheValue != null) {
            return (Product) cacheValue;
        }
        
        // 3. 缓存不存在,查询数据库
        Product product = productRepository.findById(productId).orElse(null);
        
        if (product != null) {
            redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
        } else {
            // 为防止缓存穿透,缓存空值(可选)
            redisTemplate.opsForValue().set(cacheKey, "NULL", 60, TimeUnit.SECONDS);
        }
        
        return product;
    }
}

关键点

  • 布隆过滤器初始化时需要加载所有可能存在的数据ID
  • 选择合适的误判率和容量,平衡内存使用和准确性
  • 适用于数据集合相对固定、新增数据不频繁的场景

二、缓存击穿:热点Key失效引发的灾难

问题本质

缓存击穿是指大量请求同时访问某个热点Key,而这个Key恰好在此时失效,导致所有请求直接打到数据库上。这与二八定律密切相关------系统中20%的热点数据往往承担了80%的访问量。

典型场景

  • 电商大促期间的秒杀商品
  • 突发热点事件相关的数据
  • 首页推荐内容

实战解决方案

客户端 A 客户端 B 缓存层 (Redis) 分布式锁 数据库 热点Key: product_100 已过期 获取 product_100 未命中 获取 product_100 未命中 尝试获取锁 (lock:product_100) 获取成功 尝试获取锁 (lock:product_100) 获取失败 短暂休眠,稍后重试 查询数据库 返回数据 写入新数据 (带随机TTL) 写入成功 释放锁 释放成功 返回数据 重试获取 product_100 命中! 返回数据 客户端 A 客户端 B 缓存层 (Redis) 分布式锁 数据库

方案1:互斥锁重建缓存

java 复制代码
@Service
public class HotProductService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    // 使用ConcurrentHashMap作为本地锁容器
    private final Map<String, Object> localLockMap = new ConcurrentHashMap<>();
    
    public Product getHotProduct(String productId) {
        String cacheKey = "hot:product:" + productId;
        Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
        
        if (product != null) {
            return product;
        }
        
        // 缓存不存在,尝试获取本地锁
        Object lock = localLockMap.computeIfAbsent(cacheKey, k -> new Object());
        
        synchronized (lock) {
            try {
                // 双重检查,防止多个线程同时重建缓存
                product = (Product) redisTemplate.opsForValue().get(cacheKey);
                if (product != null) {
                    return product;
                }
                
                // 从数据库加载数据
                product = productRepository.findById(productId)
                        .orElseThrow(() -> new RuntimeException("Product not found"));
                
                // 设置缓存,注意设置随机过期时间
                long expireTime = 300 + new Random().nextInt(300); // 5-10分钟随机
                redisTemplate.opsForValue().set(cacheKey, product, expireTime, TimeUnit.SECONDS);
                
                return product;
            } finally {
                // 移除本地锁(注意:不能直接remove,可能导致并发问题)
                localLockMap.computeIfPresent(cacheKey, (k, v) -> null);
            }
        }
    }
}

方案2:逻辑过期(永不过期策略)

java 复制代码
// 自定义缓存值结构,包含数据和逻辑过期时间
@Data
public class CacheData<T> {
    private T data;
    private long expireTime; // 逻辑过期时间戳
}

@Service
public class HotProductService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    // 逻辑过期时间,比如设置为10分钟
    private static final long LOGICAL_EXPIRE_TIME = 600;
    
    public Product getHotProductWithLogicalExpire(String productId) {
        String cacheKey = "hot:product:logical:" + productId;
        CacheData<Product> cacheData = (CacheData<Product>) redisTemplate.opsForValue().get(cacheKey);
        
        // 缓存存在且未逻辑过期,直接返回
        if (cacheData != null && cacheData.getExpireTime() > System.currentTimeMillis()) {
            return cacheData.getData();
        }
        
        // 缓存不存在或已过期,尝试重建缓存
        if (cacheData == null || cacheData.getExpireTime() <= System.currentTimeMillis()) {
            // 使用Redis分布式锁,防止缓存重建时的并发问题
            String lockKey = "lock:" + cacheKey;
            boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "LOCK", 3, TimeUnit.SECONDS);
            
            if (locked) {
                try {
                    // 重新检查,防止其他线程已经重建了缓存
                    cacheData = (CacheData<Product>) redisTemplate.opsForValue().get(cacheKey);
                    if (cacheData == null || cacheData.getExpireTime() <= System.currentTimeMillis()) {
                        // 从数据库加载数据
                        Product product = productRepository.findById(productId)
                                .orElseThrow(() -> new RuntimeException("Product not found"));
                        
                        // 设置新的逻辑过期时间
                        long newExpireTime = System.currentTimeMillis() + LOGICAL_EXPIRE_TIME * 1000;
                        CacheData<Product> newCacheData = new CacheData<>();
                        newCacheData.setData(product);
                        newCacheData.setExpireTime(newExpireTime);
                        
                        // 永久缓存(实际设置较长过期时间)
                        redisTemplate.opsForValue().set(cacheKey, newCacheData, 24, TimeUnit.HOURS);
                    }
                } finally {
                    // 释放锁
                    redisTemplate.delete(lockKey);
                }
            }
        }
        
        // 无论是否成功重建缓存,都返回当前缓存中的数据(可能过期)
        cacheData = (CacheData<Product>) redisTemplate.opsForValue().get(cacheKey);
        return cacheData != null ? cacheData.getData() : null;
    }
}

关键点

  • 互斥锁方案简单但可能造成请求阻塞
  • 逻辑过期方案能保证服务可用性,但可能返回短暂过期数据
  • 对于极高并发的热点Key,建议结合二级缓存(本地缓存+分布式缓存)

三、缓存雪崩:大规模缓存失效的连锁反应

问题本质

缓存雪崩是指大量缓存数据在同一时刻失效 ,或者缓存服务整体不可用,导致所有请求直接打到数据库,可能引发整个系统崩溃。

典型场景

  • 系统启动时大量缓存同时设置相同过期时间
  • Redis集群故障或网络问题
  • 缓存预热不足导致大流量涌入

Java实战解决方案

客户端 本地缓存 (Caffeine) 分布式缓存 (Redis) 熔断器 (Hystrix/Sentinel) 数据库 请求数据 (Key: config_001) 查询本地缓存 未命中 查询分布式缓存 未命中 (可能已集体过期) 查询数据库 返回数据 写入数据 (TTL: 1h + 随机 0-10min) 写入成功 回填本地缓存 (TTL: 5min) 回填成功 返回真实数据 保护数据库,快速失败 返回降级数据/错误 alt [系统健康 (熔断器关闭)] [系统过载 (熔断器打开)] 客户端 本地缓存 (Caffeine) 分布式缓存 (Redis) 熔断器 (Hystrix/Sentinel) 数据库

方案1:差异化过期时间

java 复制代码
@Service
public class CacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 基础过期时间(分钟)
    private static final int BASE_EXPIRE_MINUTES = 30;
    // 随机延长范围(分钟)
    private static final int RANDOM_EXTEND_MINUTES = 10;
    
    public void setCacheWithRandomExpire(String key, Object value) {
        // 随机生成额外的过期时间(0-10分钟)
        int randomExtra = new Random().nextInt(RANDOM_EXTEND_MINUTES);
        int totalExpireMinutes = BASE_EXPIRE_MINUTES + randomExtra;
        
        redisTemplate.opsForValue().set(key, value, totalExpireMinutes, TimeUnit.MINUTES);
    }
}

方案2:缓存高可用架构

在Spring Boot中配置Redis Cluster:

yaml 复制代码
# application.yml
spring:
  redis:
    cluster:
      nodes:
        - redis-node1:6379
        - redis-node2:6379
        - redis-node3:6379
        - redis-node4:6379
        - redis-node5:6379
        - redis-node6:6379
    lettuce:
      cluster:
        refresh:
          adaptive: true
          period: 30s
      pool:
        max-active: 200
        max-wait: -1ms
        max-idle: 10
        min-idle: 5

方案3:服务降级与熔断

使用Resilience4j实现服务降级:

java 复制代码
@Service
public class ProductServiceWithFallback {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    private final CircuitBreaker circuitBreaker;
    
    public ProductServiceWithFallback() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
                .failureRateThreshold(50)  // 失败率阈值
                .waitDurationInOpenState(Duration.ofMillis(1000)) // 熔断后等待时间
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(5)  // 滑动窗口大小
                .build();
        
        circuitBreaker = CircuitBreaker.of("productService", config);
    }
    
    public Product getProductWithFallback(String productId) {
        Supplier<Product> supplier = CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
            String cacheKey = "product:" + productId;
            Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
            
            if (product != null) {
                return product;
            }
            
            // 缓存不存在,查询数据库
            product = productRepository.findById(productId).orElse(null);
            
            if (product != null) {
                redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
            }
            
            return product;
        });
        
        // 当熔断开启时,使用降级策略
        Try<Product> result = Try.ofSupplier(supplier)
                .recover(throwable -> {
                    // 熔断或异常时,返回降级数据
                    return getFallbackProduct(productId);
                });
        
        return result.get();
    }
    
    private Product getFallbackProduct(String productId) {
        // 可以返回默认产品、空对象或从其他渠道获取数据
        Product fallbackProduct = new Product();
        fallbackProduct.setId(productId);
        fallbackProduct.setName("商品信息加载中...");
        fallbackProduct.setPrice(0);
        fallbackProduct.setStock(0);
        fallbackProduct.setFallback(true);
        return fallbackProduct;
    }
}

关键点

  • 差异化过期时间是预防缓存雪崩最简单有效的方法
  • Redis Cluster是实现缓存高可用的行业标准方案
  • 服务降级确保在缓存不可用时系统仍能提供基本服务

四、实战:缓存防护体系

让我们通过一个大促场景,整合应用上述所有技术方案:

java 复制代码
/**
 * 电商大促场景下的商品服务
 * 集成缓存穿透、缓存击穿、缓存雪崩防护
 */
@Service
@RequiredArgsConstructor
public class FlashSaleService {
    
    private final RedisTemplate<String, Object> redisTemplate;
    private final ProductRepository productRepository;
    private final BloomFilter<String> productBloomFilter;
    private final CircuitBreaker circuitBreaker;
    private final Map<String, Object> localLockMap = new ConcurrentHashMap<>();
    
    // 缓存配置
    private static final String PRODUCT_CACHE_PREFIX = "flashsale:product:";
    private static final String NULL_CACHE_PREFIX = "flashsale:null:";
    private static final long BASE_CACHE_EXPIRE_MINUTES = 30;
    private static final long NULL_CACHE_EXPIRE_SECONDS = 60;
    private static final String NULL_VALUE = "NULL";
    
    /**
     * 获取秒杀商品信息(完整防护方案)
     */
    public Product getFlashSaleProduct(String productId) {
        // 1. 布隆过滤器检查(防缓存穿透第一道防线)
        if (!productBloomFilter.mightContain(productId)) {
            return null;
        }
        
        // 2. 尝试从缓存获取
        String cacheKey = PRODUCT_CACHE_PREFIX + productId;
        Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
        
        // 3. 缓存存在且不是空值
        if (cacheValue != null) {
            if (NULL_VALUE.equals(cacheValue)) {
                return null;
            }
            return (Product) cacheValue;
        }
        
        // 4. 缓存为空,使用熔断器保护数据库
        return circuitBreaker.executeSupplier(() -> {
            // 5. 互斥锁防止缓存击穿
            Object lock = localLockMap.computeIfAbsent(cacheKey, k -> new Object());
            synchronized (lock) {
                try {
                    // 双重检查
                    cacheValue = redisTemplate.opsForValue().get(cacheKey);
                    if (cacheValue != null) {
                        return (Product) (NULL_VALUE.equals(cacheValue) ? null : cacheValue);
                    }
                    
                    // 6. 查询数据库
                    Product product = productRepository.findById(productId).orElse(null);
                    
                    if (product != null) {
                        // 设置随机过期时间(防缓存雪崩)
                        int randomExtra = new Random().nextInt(10);
                        redisTemplate.opsForValue().set(
                            cacheKey, 
                            product, 
                            BASE_CACHE_EXPIRE_MINUTES + randomExtra, 
                            TimeUnit.MINUTES
                        );
                        return product;
                    }
                    
                    // 7. 数据库不存在,缓存空值(防缓存穿透)
                    redisTemplate.opsForValue().set(
                        cacheKey, 
                        NULL_VALUE, 
                        NULL_CACHE_EXPIRE_SECONDS, 
                        TimeUnit.SECONDS
                    );
                    return null;
                } finally {
                    localLockMap.remove(cacheKey);
                }
            }
        }, throwable -> {
            // 8. 熔断降级处理
            return getFallbackProduct(productId);
        });
    }
    
    private Product getFallbackProduct(String productId) {
        Product fallbackProduct = new Product();
        fallbackProduct.setId(productId);
        fallbackProduct.setName("秒杀商品加载中...");
        fallbackProduct.setPrice(0);
        fallbackProduct.setStock(0);
        fallbackProduct.setFallback(true);
        return fallbackProduct;
    }
    
    /**
     * 缓存预热(大促前执行)
     */
    @PostConstruct
    public void warmUpCache() {
        List<String> hotProductIds = productRepository.findTop100HotProducts();
        hotProductIds.forEach(productId -> {
            Product product = productRepository.findById(productId).orElse(null);
            if (product != null) {
                // 预热缓存,设置随机过期时间
                int randomExtra = new Random().nextInt(10);
                redisTemplate.opsForValue().set(
                    PRODUCT_CACHE_PREFIX + productId,
                    product,
                    BASE_CACHE_EXPIRE_MINUTES + randomExtra,
                    TimeUnit.MINUTES
                );
            }
        });
        System.out.println("缓存预热完成,已加载" + hotProductIds.size() + "个热门商品");
    }
}

五、缓存稳定性保障最佳实践

1. 缓存命中率监控

java 复制代码
/**
 * 缓存命中率监控组件
 */
@Component
public class CacheMonitor {
    
    private final AtomicLong cacheHits = new AtomicLong(0);
    private final AtomicLong cacheMisses = new AtomicLong(0);
    
    @Autowired
    public CacheMonitor(CacheManager cacheManager) {
        // 注册缓存事件监听器
        if (cacheManager instanceof RedisCacheManager) {
            RedisCacheManager redisCacheManager = (RedisCacheManager) cacheManager;
            redisCacheManager.getCacheWriter().getRedisConnectionFactory()
                .getConnection()
                .setKeySerializer(new StringRedisSerializer());
            
            redisCacheManager.getCacheWriter().getRedisConnectionFactory()
                .getConnection()
                .setValueSerializer(new JdkSerializationRedisSerializer());
        }
    }
    
    public void recordHit() {
        cacheHits.incrementAndGet();
    }
    
    public void recordMiss() {
        cacheMisses.incrementAndGet();
    }
    
    public double getHitRate() {
        long hits = cacheHits.get();
        long misses = cacheMisses.get();
        long total = hits + misses;
        return total == 0 ? 0 : (double) hits / total;
    }
    
    @Scheduled(fixedRate = 60000)
    public void logHitRate() {
        double hitRate = getHitRate();
        System.out.println("缓存命中率: " + String.format("%.2f%%", hitRate * 100));
        
        // 命中率低于阈值时告警
        if (hitRate < 0.9) {
            System.out.println("警告:缓存命中率低于90%!");
            // 实际应用中可以发送邮件或短信告警
        }
    }
}

2. 缓存服务压测

在生产环境部署前,务必对缓存服务进行压力测试:

java 复制代码
/**
 * Redis压测工具类
 */
public class RedisStressTest {
    
    private static final int THREAD_COUNT = 50;
    private static final int REQUESTS_PER_THREAD = 1000;
    
    public static void main(String[] args) {
        String redisHost = "localhost";
        int redisPort = 6379;
        
        try (JedisPool jedisPool = new JedisPool(redisHost, redisPort)) {
            ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
            CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
            
            long startTime = System.currentTimeMillis();
            
            for (int i = 0; i < THREAD_COUNT; i++) {
                executor.submit(() -> {
                    try (Jedis jedis = jedisPool.getResource()) {
                        for (int j = 0; j < REQUESTS_PER_THREAD; j++) {
                            String key = "test:key:" + j;
                            String value = "value:" + j;
                            
                            // SET操作
                            jedis.set(key, value);
                            // GET操作
                            jedis.get(key);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        latch.countDown();
                    }
                });
            }
            
            latch.await();
            long endTime = System.currentTimeMillis();
            
            long totalTime = endTime - startTime;
            long totalRequests = THREAD_COUNT * REQUESTS_PER_THREAD * 2; // SET+GET
            double qps = (double) totalRequests / (totalTime / 1000.0);
            
            System.out.println("测试完成,总请求数: " + totalRequests);
            System.out.println("总耗时: " + totalTime + "ms");
            System.out.println("QPS: " + String.format("%.2f", qps));
            
            executor.shutdown();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

3. 关键指标监控体系

指标类别 关键指标 健康阈值 监控频率
缓存数据 缓存命中率 ≥90% (大促≥99%) 实时
缓存使用率 ≤70% 5分钟
平均响应时间 ≤5ms 实时
缓存服务 CPU使用率 ≤70% 1分钟
内存使用率 ≤80% 1分钟
连接数 ≤最大连接数的80% 1分钟
持久化状态 RDB/AOF正常 5分钟

六、总结

我们系统性地分析了缓存应用中的三大经典问题,并提供了基于Java生态的完整解决方案。作为Java开发者,在设计缓存系统时,应当牢记以下原则:

  1. 预防优于补救:在系统设计阶段就考虑缓存问题,而非等到问题发生
  2. 分层防御策略:针对不同问题场景,采用多层防护机制
  3. 数据驱动决策:通过监控指标指导缓存策略调整
  4. 场景化解决方案:没有银弹,需根据业务特点选择合适方案

特别提醒 :在实际项目中,曾见过不少团队过度依赖缓存,将缓存视为解决所有性能问题的万能钥匙。实际上,缓存只是系统性能优化的手段之一,合理的数据库设计、索引优化、代码重构同样重要。当你的缓存命中率持续低于70%时,或许应该先思考:是不是缓存策略有问题,还是根本不需要缓存这些数据?

相关推荐
没事学AI5 小时前
Caffeine三种缓存过期策略总结:原理、实战与调优
java·缓存·caffeine·缓存穿透防护·caffeine缓存
Micro麦可乐5 小时前
为什么两个看似相等的 Integer 却不相等?一次诡异的缓存折扣商品 BUG 排查
java·缓存·bug·包装类判断·integer判断
Never_z&y5 小时前
HTTP学习之路:代理中的缓存投毒
网络·网络安全·缓存
echoyu.5 小时前
BUS-消息总线
分布式·spring cloud·微服务·架构·bus
敲上瘾9 小时前
Docker多容器编排:Compose 实战教程
linux·运维·docker·容器·架构
love530love10 小时前
EPGF 架构下的 Python 环境变量设置建议——Anaconda 路径精简后暴露 python 及工具到环境变量的配置记录 [三]
开发语言·人工智能·windows·python·架构·conda·epgf 架构
timmy-uav10 小时前
MissionPlanner架构梳理之(十八)视频流
架构·系统架构·无人机·开源地面站·missionplanner
阿里云云原生11 小时前
移动端性能监控探索:鸿蒙 NEXT 探针架构与技术实现
华为·架构·harmonyos
杨杨杨大侠11 小时前
探索 Event 框架实战指南:微服务系统中的事件驱动通信:
java·spring boot·微服务·云原生·架构·系统架构