SpringBoot + Redis 缓存击穿防护 + 互斥重建:热点 Key 过期时,仅一个线程回源 DB

导语

在高并发系统中,缓存是提升性能的关键手段。然而,当热点 Key 过期时,大量并发请求同时穿透缓存直接访问数据库,可能导致数据库压力骤增甚至宕机。这种现象被称为"缓存击穿"。

本文将介绍如何在 SpringBoot 应用中实现 Redis 缓存击穿防护和互斥重建机制,确保热点 Key 过期时,只有一个线程回源数据库,其他线程等待或使用旧数据,从而保护数据库免受高并发冲击。

一、缓存击穿的概念与危害

1.1 什么是缓存击穿

缓存击穿是指某个热点 Key 在高并发访问时突然过期,导致大量并发请求同时穿透缓存直接访问数据库的现象。

场景描述

  1. 某个商品信息被大量用户频繁访问
  2. 该商品的缓存 Key 设置了过期时间
  3. 缓存过期瞬间,大量请求同时到达
  4. 所有请求都发现缓存不存在,同时访问数据库
  5. 数据库瞬间承受巨大压力,可能导致宕机

1.2 缓存击穿与相关概念的区别

概念描述解决方案
缓存击穿热点 Key 过期,大量请求同时访问数据库互斥锁、永不过期
缓存穿透查询不存在的数据,请求直接访问数据库布隆过滤器、缓存空值
缓存雪崩大量 Key 同时过期,导致数据库压力骤增过期时间随机化、预热

1.3 缓存击穿的危害

危害描述
数据库压力大量并发请求同时访问数据库
系统崩溃数据库可能因过载而崩溃
响应延迟请求响应时间显著增加
用户体验用户等待时间过长,体验下降
成本增加需要增加数据库资源应对峰值

二、技术方案设计

2.1 架构设计

flowchart TD
    subgraph 客户端层
        A[客户端请求] -->|请求| B[SpringBoot 应用]
    end
    
    subgraph 缓存层
        B -->|查询| C[Redis]
        C -->|缓存命中| B
        C -->|缓存未命中| D[互斥锁检查]
    end
    
    subgraph 互斥层
        D -->|获取锁成功| E[回源数据库]
        D -->|获取锁失败| F[等待或使用旧数据]
    end
    
    subgraph 数据层
        E -->|查询| G[数据库]
        G -->|返回数据| E
    end
    
    subgraph 缓存重建
        E -->|写入缓存| C
        F -->|等待重建完成| C
    end

2.2 核心组件

  1. 缓存服务:负责缓存的读写操作
  2. 互斥锁服务:负责分布式锁的获取和释放
  3. 缓存击穿防护服务:整合缓存和互斥锁,实现击穿防护
  4. 业务服务:使用缓存击穿防护的业务逻辑

2.3 技术选型

技术版本用途
SpringBoot2.7.14应用框架
Spring Data Redis2.7.0Redis 客户端
Redis7.0+缓存存储
Lettuce6.1.0Redis 连接池
Lombok1.18.0简化代码

三、核心实现

3.1 依赖配置

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- Redisson (可选,提供更强大的分布式锁功能) -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.20.0</version>
    </dependency>
    
    <!-- Spring Boot Actuator -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

3.2 缓存击穿防护服务

CacheBreakdownProtectionService.java

@Service
@Slf4j
public class CacheBreakdownProtectionService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private DistributedLockService distributedLockService;
    
    private static final String CACHE_PREFIX = "cache:";
    private static final String LOCK_PREFIX = "lock:";
    private static final long DEFAULT_CACHE_EXPIRE_TIME = 3600; // 默认缓存过期时间 1 小时
    private static final long DEFAULT_LOCK_WAIT_TIME = 3000; // 默认获取锁等待时间 3 秒
    private static final long DEFAULT_LOCK_EXPIRE_TIME = 10000; // 默认锁过期时间 10 秒
    
    /**
     * 获取缓存数据,带击穿防护
     * @param key 缓存键
     * @param dataLoader 数据加载器
     * @param <T> 数据类型
     * @return 缓存数据
     */
    public <T> T getWithProtection(String key, DataLoader<T> dataLoader) {
        return getWithProtection(key, dataLoader, DEFAULT_CACHE_EXPIRE_TIME);
    }
    
    /**
     * 获取缓存数据,带击穿防护
     * @param key 缓存键
     * @param dataLoader 数据加载器
     * @param cacheExpireTime 缓存过期时间(秒)
     * @param <T> 数据类型
     * @return 缓存数据
     */
    public <T> T getWithProtection(String key, DataLoader<T> dataLoader, long cacheExpireTime) {
        String cacheKey = CACHE_PREFIX + key;
        
        // 1. 尝试从缓存获取
        T cachedData = getFromCache(cacheKey);
        if (cachedData != null) {
            log.debug("Cache hit: {}", key);
            return cachedData;
        }
        
        // 2. 缓存未命中,尝试获取互斥锁
        String lockKey = LOCK_PREFIX + key;
        boolean lockAcquired = distributedLockService.acquireLock(lockKey, DEFAULT_LOCK_EXPIRE_TIME, DEFAULT_LOCK_WAIT_TIME);
        
        if (lockAcquired) {
            // 3. 获取锁成功,负责重建缓存
            try {
                // 双重检查,防止其他线程已经重建了缓存
                cachedData = getFromCache(cacheKey);
                if (cachedData != null) {
                    log.debug("Cache hit after acquiring lock: {}", key);
                    return cachedData;
                }
                
                // 回源数据库加载数据
                log.info("Loading data from database: {}", key);
                T data = dataLoader.load();
                
                if (data != null) {
                    // 写入缓存
                    setToCache(cacheKey, data, cacheExpireTime);
                    log.info("Cache rebuilt: {}", key);
                }
                
                return data;
            } finally {
                // 释放锁
                distributedLockService.releaseLock(lockKey);
            }
        } else {
            // 4. 获取锁失败,等待并重试
            log.info("Failed to acquire lock, waiting for cache rebuild: {}", key);
            return waitForCacheRebuild(cacheKey, dataLoader, cacheExpireTime);
        }
    }
    
    /**
     * 从缓存获取数据
     */
    @SuppressWarnings("unchecked")
    private <T> T getFromCache(String key) {
        try {
            return (T) redisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            log.error("Error getting from cache: {}", key, e);
            return null;
        }
    }
    
    /**
     * 写入缓存
     */
    private <T> void setToCache(String key, T data, long expireTime) {
        try {
            redisTemplate.opsForValue().set(key, data, expireTime, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error("Error setting to cache: {}", key, e);
        }
    }
    
    /**
     * 等待缓存重建完成
     */
    private <T> T waitForCacheRebuild(String cacheKey, DataLoader<T> dataLoader, long cacheExpireTime) {
        // 短暂等待后重试
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        // 重试从缓存获取
        T cachedData = getFromCache(cacheKey);
        if (cachedData != null) {
            log.debug("Cache hit after waiting: {}", cacheKey);
            return cachedData;
        }
        
        // 如果等待后缓存仍未重建,直接回源数据库
        log.warn("Cache not rebuilt after waiting, loading from database: {}", cacheKey);
        return dataLoader.load();
    }
    
    /**
     * 数据加载器接口
     */
    @FunctionalInterface
    public interface DataLoader<T> {
        T load();
    }
}

3.3 分布式锁服务

DistributedLockService.java

@Service
@Slf4j
public class DistributedLockService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String LOCK_VALUE_PREFIX = "lock:";
    
    /**
     * 获取分布式锁
     * @param lockKey 锁键
     * @param expireTime 锁过期时间(毫秒)
     * @param waitTime 获取锁等待时间(毫秒)
     * @return 是否获取成功
     */
    public boolean acquireLock(String lockKey, long expireTime, long waitTime) {
        String lockValue = LOCK_VALUE_PREFIX + UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();
        
        try {
            while (System.currentTimeMillis() - startTime < waitTime) {
                // 使用 SETNX 命令获取锁
                Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
                if (Boolean.TRUE.equals(success)) {
                    log.debug("Acquired distributed lock: {}", lockKey);
                    return true;
                }
                
                // 短暂休眠后重试
                Thread.sleep(50);
            }
            
            log.debug("Failed to acquire distributed lock: {} after {}ms", lockKey, waitTime);
            return false;
        } catch (Exception e) {
            log.error("Error acquiring distributed lock: {}", lockKey, e);
            return false;
        }
    }
    
    /**
     * 释放分布式锁
     * @param lockKey 锁键
     */
    public void releaseLock(String lockKey) {
        try {
            redisTemplate.delete(lockKey);
            log.debug("Released distributed lock: {}", lockKey);
        } catch (Exception e) {
            log.error("Error releasing distributed lock: {}", lockKey, e);
        }
    }
    
    /**
     * 检查锁是否存在
     * @param lockKey 锁键
     * @return 是否存在
     */
    public boolean isLockExists(String lockKey) {
        try {
            return redisTemplate.hasKey(lockKey);
        } catch (Exception e) {
            log.error("Error checking lock existence: {}", lockKey, e);
            return false;
        }
    }
}

3.4 业务服务实现

ProductService.java

@Service
@Slf4j
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private CacheBreakdownProtectionService cacheProtectionService;
    
    private static final long PRODUCT_CACHE_EXPIRE_TIME = 1800; // 商品缓存过期时间 30 分钟
    
    /**
     * 获取商品信息(带缓存击穿防护)
     * @param productId 商品ID
     * @return 商品信息
     */
    public Product getProductById(Long productId) {
        String cacheKey = "product:" + productId;
        
        return cacheProtectionService.getWithProtection(cacheKey, () -> {
            // 回源数据库加载商品信息
            log.info("Loading product from database: {}", productId);
            Product product = productRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("Product not found: " + productId));
            
            // 更新访问时间
            product.setLastAccessTime(LocalDateTime.now());
            productRepository.save(product);
            
            return product;
        }, PRODUCT_CACHE_EXPIRE_TIME);
    }
    
    /**
     * 获取商品库存(带缓存击穿防护)
     * @param productId 商品ID
     * @return 库存数量
     */
    public Integer getProductStock(Long productId) {
        String cacheKey = "product:stock:" + productId;
        
        return cacheProtectionService.getWithProtection(cacheKey, () -> {
            // 回源数据库加载库存信息
            log.info("Loading product stock from database: {}", productId);
            Product product = productRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("Product not found: " + productId));
            
            return product.getStock();
        }, PRODUCT_CACHE_EXPIRE_TIME);
    }
    
    /**
     * 更新商品信息
     * @param productId 商品ID
     * @param product 商品信息
     * @return 更新后的商品信息
     */
    public Product updateProduct(Long productId, Product product) {
        // 更新数据库
        Product existingProduct = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found: " + productId));
        
        existingProduct.setName(product.getName());
        existingProduct.setPrice(product.getPrice());
        existingProduct.setStock(product.getStock());
        existingProduct.setUpdateTime(LocalDateTime.now());
        
        Product updatedProduct = productRepository.save(existingProduct);
        
        // 删除缓存,强制下次访问时重建
        String cacheKey = "product:" + productId;
        deleteCache(cacheKey);
        
        log.info("Product updated and cache deleted: {}", productId);
        return updatedProduct;
    }
    
    /**
     * 删除缓存
     */
    private void deleteCache(String key) {
        try {
            String cacheKey = "cache:" + key;
            redisTemplate.delete(cacheKey);
            log.debug("Cache deleted: {}", key);
        } catch (Exception e) {
            log.error("Error deleting cache: {}", key, e);
        }
    }
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
}

3.5 控制器实现

ProductController.java

@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @Autowired
    private ProductService productService;
    
    /**
     * 获取商品信息
     */
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        try {
            Product product = productService.getProductById(id);
            return ResponseEntity.ok(product);
        } catch (Exception e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    /**
     * 获取商品库存
     */
    @GetMapping("/{id}/stock")
    public ResponseEntity<Integer> getProductStock(@PathVariable Long id) {
        try {
            Integer stock = productService.getProductStock(id);
            return ResponseEntity.ok(stock);
        } catch (Exception e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    /**
     * 更新商品信息
     */
    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(@PathVariable Long id, @RequestBody Product product) {
        try {
            Product updatedProduct = productService.updateProduct(id, product);
            return ResponseEntity.ok(updatedProduct);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(null);
        }
    }
}

3.6 Redis 配置

RedisConfig.java

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        
        // key 采用 String 的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash 的 key 也采用 String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value 序列化方式采用 jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 的 value 序列化方式采用 jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
    
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)))
            .disableCachingNullValues();
        
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .build();
    }
}

四、生产级实现

4.1 配置文件

application.yml

# 应用配置
spring:
  application:
    name: cache-breakdown-protection
  
  # Redis 配置
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
    database: ${REDIS_DATABASE:0}
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: -1ms
    timeout: 3000ms

# 服务器配置
server:
  port: 8080
  servlet:
    context-path: /

# 缓存击穿防护配置
cache:
  breakdown:
    protection:
      # 默认缓存过期时间(秒)
      default-cache-expire-time: 3600
      # 默认获取锁等待时间(毫秒)
      default-lock-wait-time: 3000
      # 默认锁过期时间(毫秒)
      default-lock-expire-time: 10000
      # 缓存键前缀
      cache-key-prefix: cache:
      # 锁键前缀
      lock-key-prefix: lock:
      # 是否启用日志
      enable-logging: true

# 监控配置
management:
  endpoints:
    web:
      exposure:
        include: "health,info,metrics,prometheus"
  endpoint:
    health:
      show-details: always

# 日志配置
logging:
  level:
    com.example.cache: info
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

4.2 性能优化

1. 连接池优化

spring:
  redis:
    lettuce:
      pool:
        max-active: 20  # 最大连接数
        max-idle: 10   # 最大空闲连接
        min-idle: 5    # 最小空闲连接
        max-wait: 3000ms  # 最大等待时间

2. 序列化优化

  • 使用 Protobuf 或 Kryo 替代 Jackson
  • 减少序列化后的数据大小
  • 提高序列化和反序列化速度

3. 缓存预热

  • 系统启动时预热热点数据
  • 使用定时任务刷新热点数据
  • 避免系统启动时的缓存击穿

4.3 监控与告警

1. 监控指标

  • 缓存命中率
  • 缓存击穿次数
  • 锁获取失败次数
  • 数据库访问次数

2. 告警配置

# Prometheus 告警规则
groups:
- name: cache-breakdown-alerts
  rules:
  - alert: CacheBreakdownHigh
    expr: cache_breakdown_count > 100
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Cache breakdown rate is high"
      description: "Cache breakdown count: {{ $value }}"

3. Grafana 仪表盘
创建 Grafana 仪表盘,展示:

  • 缓存命中率趋势
  • 缓存击穿次数
  • 数据库访问次数
  • 系统响应时间

4.4 安全配置

1. Redis 认证

spring:
  redis:
    password: ${REDIS_PASSWORD}

2. 网络隔离

  • 使用 Redis 的 ACL 功能
  • 限制 Redis 的访问 IP
  • 使用 SSL/TLS 加密连接

3. 数据加密

  • 敏感数据加密后存储
  • 使用 Redis 的加密模块
  • 定期更换加密密钥

五、最佳实践

5.1 缓存设计最佳实践

1. 缓存键设计

  • 使用有意义的键名
  • 包含业务前缀
  • 避免键名冲突

2. 缓存过期时间

  • 根据数据更新频率设置
  • 热点数据设置较长过期时间
  • 冷数据设置较短过期时间

3. 缓存数据结构

  • 选择合适的数据结构
  • 避免存储过大的对象
  • 考虑使用 Hash 结构存储复杂对象

5.2 互斥锁最佳实践

1. 锁粒度

  • 使用细粒度锁,减少锁竞争
  • 避免使用全局锁
  • 根据业务需求设计锁粒度

2. 锁超时

  • 设置合理的锁超时时间
  • 避免锁超时时间过长
  • 考虑业务执行时间

3. 锁释放

  • 始终在 finally 块中释放锁
  • 避免死锁
  • 处理锁释放失败的情况

5.3 性能优化最佳实践

1. 缓存预热

  • 系统启动时预热热点数据
  • 使用定时任务刷新热点数据
  • 避免系统启动时的缓存击穿

2. 批量操作

  • 使用 Pipeline 减少网络开销
  • 批量获取和设置缓存
  • 减少与 Redis 的交互次数

3. 连接池管理

  • 合理配置连接池大小
  • 监控连接池使用情况
  • 及时释放空闲连接

六、案例分析

6.1 案例一:电商商品详情

场景

  • 电商平台商品详情页访问量巨大
  • 商品信息缓存过期时,大量请求同时访问数据库
  • 数据库压力骤增,导致响应变慢

解决方案

  1. 实现缓存击穿防护机制
  2. 使用互斥锁确保只有一个线程回源数据库
  3. 其他线程等待或使用旧数据

效果

  • 数据库压力显著降低
  • 响应时间保持稳定
  • 系统可用性提高

6.2 案例二:秒杀活动

场景

  • 秒杀活动开始时,大量用户同时访问商品信息
  • 商品库存缓存过期,导致大量请求访问数据库
  • 数据库无法承受高并发,导致活动失败

解决方案

  1. 预热秒杀商品缓存
  2. 实现缓存击穿防护
  3. 使用互斥锁确保库存更新的原子性

效果

  • 秒杀活动顺利进行
  • 数据库压力可控
  • 用户体验良好

6.3 案例三:新闻热点

场景

  • 新闻热点文章被大量用户访问
  • 文章缓存过期时,大量请求同时访问数据库
  • 数据库响应变慢,影响用户体验

解决方案

  1. 识别热点文章
  2. 设置较长的缓存过期时间
  3. 实现缓存击穿防护

效果

  • 热点文章访问流畅
  • 数据库压力可控
  • 用户体验提升

七、未来发展趋势

7.1 技术演进

1. 智能缓存管理

  • 基于 AI 的缓存策略优化
  • 自动识别热点数据
  • 动态调整缓存参数

2. 多级缓存

  • 本地缓存 + 分布式缓存
  • 缓存层级自动切换
  • 缓存一致性保证

3. 云原生支持

  • 容器化部署
  • 服务网格集成
  • 云平台原生缓存服务

7.2 应用扩展

1. 跨数据中心缓存

  • 多数据中心缓存同步
  • 缓存一致性保证
  • 故障自动切换

2. 边缘缓存

  • 边缘节点的缓存部署
  • 就近访问热点数据
  • 降低网络延迟

3. 智能预加载

  • 基于用户行为的预加载
  • 预测热点数据
  • 提前加载到缓存

7.3 行业应用

1. 金融行业

  • 高可靠性缓存
  • 严格的数据一致性
  • 合规性和审计要求

2. 电商行业

  • 高并发缓存
  • 热点数据识别
  • 秒杀活动支持

3. 内容行业

  • 内容分发网络
  • 热点内容缓存
  • 用户体验优化

小结

本文介绍了 SpringBoot 应用中实现 Redis 缓存击穿防护和互斥重建的完整解决方案,包括:

  • 缓存击穿概念:理解缓存击穿的原理和危害
  • 互斥重建机制:确保只有一个线程回源数据库
  • 核心实现:缓存击穿防护服务、分布式锁服务、业务服务
  • 生产级配置:性能优化、监控告警、安全配置
  • 案例分析:电商商品、秒杀活动、新闻热点
  • 最佳实践:缓存设计、互斥锁、性能优化
  • 未来趋势:智能化、多级缓存、云原生支持

通过实施这些技术方案,您可以建立一个高性能、高可用的缓存系统,防止缓存击穿导致的数据库崩溃,确保系统在高并发场景下的稳定运行。

互动话题

  1. 您在项目中遇到过哪些缓存击穿的挑战?是如何解决的?
  2. 您对本文介绍的缓存击穿防护方案有什么改进建议?
  3. 您认为在微服务架构中,缓存击穿防护有哪些新的挑战?
  4. 您对未来缓存管理技术的发展有什么看法?

欢迎在评论区分享您的经验和看法!


标题:SpringBoot + Redis 缓存击穿防护 + 互斥重建:热点 Key 过期时,仅一个线程回源 DB
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/13/1773120113062.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消