Skip to content

背景

在很多大型网站中,某些数据(比如热门文章、热点商品详情等)可能会成为热点,经常被大量用户请求。这可能导致数据库压力过大,进而使接口响应时间变长。为了应对这一问题,我们采取了一系列措施来减轻数据库负担,并提高接口的响应速度。

主要采用了有效的缓存策略和异步处理技术来减轻数据库负担并提高接口响应速度。

以下演示的是我们需要提供一个电商平台的商品详情接口,该接口需要执行以下操作:

  1. 获取商品的基本信息。
  2. 获取商品的用户评论。
  3. 获取推荐商品列表。

技术选型

这里面商品的基本信息和商品的评论信息我们可以适当的做一些数据预热。

数据预热基本上是要基于缓存的,缓存有很多种,本地缓存,分布式缓存等,这里我们选择了目前市面上比较常用的本地缓存+分布式缓存实现二级缓存。

本地缓存主要选择Caffeine,主要是因为Caffeine 提供了接近最优的性能,是当前 Java 中最快的缓存库之一。支持时间和大小驱逐策略,以及同步和异步加载等。同时也是Spring官方推荐的缓存框架。

分布式缓存采用的是主流的Redis。

异步化编程这里直接使用 Java 的 CompletableFuture 或 Spring 的 @Async 注解实现异步处理,以优化耗时操作。

具体实现

Caffeine 缓存配置

java
@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("dataCache");
        cacheManager.setCaffeine(caffeineCacheBuilder());
        return cacheManager;
    }

    Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(1000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .weakKeys()
                .recordStats();
    }
}

二级缓存实现

javascript
@Service
public class DataService {

    @Autowired
    private CacheManager caffeineCacheManager;

    @Autowired
    private StringRedisTemplate redisTemplate;

    public Object getData(String key) {
        // 尝试从 Caffeine 缓存获取
        Cache caffeineCache = caffeineCacheManager.getCache("dataCache");
        Object value = caffeineCache.getIfPresent(key);

        if (value == null) {
            // 尝试从 Redis 缓存获取
            value = redisTemplate.opsForValue().get(key);
            if (value == null) {
                // 缓存未命中,从数据库加载
                value = loadDataFromDB(key);
                redisTemplate.opsForValue().set(key, value);
            }
            caffeineCache.put(key, value);
        }

        return value;
    }

    private Object loadDataFromDB(String key){
      // 自己实现的从数据库中查询数据
    }
}

整个ProductService的实现:

这里我们使用 CompletableFuture, 提供了丰富的 API 来处理异步操作和转换结果。可以用来优化后台任务,比如数据的异步加载和处理。

javascript
@Service
public class ProductService {

    @Autowired
    private CacheManager caffeineCacheManager;

    @Autowired
    private StringRedisTemplate redisTemplate;

    public CompletableFuture<Product> getProductDetails(String productId) {
        return CompletableFuture.completedFuture(getCachedProduct(productId));
    }

    public CompletableFuture<List<Review>> getProductReviews(String productId) {
        return CompletableFuture.completedFuture(getCachedReviews(productId));
    }

    public CompletableFuture<List<Product>> getRecommendedProducts(String productId) {
        // 模拟数据库查询
        return CompletableFuture.completedFuture(queryRecommendations(productId));
    }


    private Product getCachedProduct(String productId) {
        return getCachedData("productCache", productId, this::queryProduct);
    }

    private List<Review> getCachedReviews(String productId) {
        return getCachedData("reviewsCache", productId, this::queryReviews);
    }

    private <T> T getCachedData(String cacheName, String key, Function<String, T> dbQuery) {
        // 尝试从 Caffeine 缓存获取
        Cache caffeineCache = caffeineCacheManager.getCache(cacheName);
        T value = caffeineCache.getIfPresent(key, T.class);
        
        if (value == null) {
            // 尝试从 Redis 缓存获取
            String cachedData = redisTemplate.opsForValue().get(key);
            if (cachedData != null) {
                value = convertStringToObject(cachedData, T.class); // 将字符串转换为对象
            } else {
                // 缓存未命中,从数据库加载
                value = dbQuery.apply(key);
                redisTemplate.opsForValue().set(key, convertObjectToString(value)); // 缓存结果
            }
            caffeineCache.put(key, value);
        }

        return value;
    }

    private Product queryProduct(String productId) {
        // 从数据库查询商品详情逻辑
    }

    private List<Review> queryReviews(String productId) {
        // 从数据库查询用户评论逻辑
    }

  	    private List<Product> queryRecommendations(String productId) {
        // 从数据库查询推荐商品逻辑
    }
    
}

编排及结果汇总:

javascript
@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/{productId}")
    public CompletableFuture<ProductDetail> getProductDetail(@PathVariable String productId) {
        CompletableFuture<Product> productFuture = productService.getProductDetails(productId);
        CompletableFuture<List<Review>> reviewsFuture = productService.getProductReviews(productId);
        CompletableFuture<List<Product>> recommendationsFuture = productService.getRecommendedProducts(productId);

        return CompletableFuture.allOf(productFuture, reviewsFuture, recommendationsFuture)
                .thenApply(v -> {
                    Product product = productFuture.join();
                    List<Review> reviews = reviewsFuture.join();
                    List<Product> recommendations = recommendationsFuture.join();
                    return new ProductDetail(product, reviews, recommendations);
                });
    }
}


public class ProductDetail {
    private Product product;
    private List<Review> reviews;
    private List<Product> recommendations;

    // 构造函数、getter 和 setter
}

以上方案,还存在两个问题没有考虑:

  • 1、缓存一致性:如何确保在数据更新时同步更新或清除缓存,如何做到缓存失效
  • 2、异常处理:在使用 CompletableFuture 时,应该考虑异常处理策略,例如使用 exceptionally 方法来处理异常情况。

接着优化。

异常处理

在使用 CompletableFuture 时,可以使用 exceptionally 方法来处理异常情况。此外,还可以使用 handle 方法,它能够同时处理正常的结果和异常情况。

javascript
@Service
public class ProductService {

    // ... 其他方法 ...

    @Async
    public CompletableFuture<Product> getProductDetails(String productId) {
        return CompletableFuture
                .supplyAsync(() -> getCachedProduct(productId))
                .exceptionally(ex -> handleException("getProductDetails", productId, ex));
    }

    @Async
    public CompletableFuture<List<Review>> getProductReviews(String productId) {
        return CompletableFuture
                .supplyAsync(() -> getCachedReviews(productId))
                .exceptionally(ex -> handleException("getProductReviews", productId, ex));
    }

    private <T> T handleException(String methodName, String key, Throwable ex) {
        // 记录日志
        log.error("Error in " + methodName + " for key " + key, ex);
        // 可以返回一个默认值或 null,或者重新抛出异常
        return null;
    }
}

缓存一致性

保证缓存一致性,有很多方案,我们可以采用延迟双删的方案,也可以采用cache aside的方案。这里用延迟双删实现。

我们使用 Spring 的 @Scheduled注解来实现定时任务做第二次的删除动作。

javascript
@Service
public class ProductService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private final BlockingQueue<String> cacheDeletionQueue = new LinkedBlockingQueue<>();

    public void updateProduct(Product product) {
        // 第一次删除缓存
        deleteCache(product.getId());

        // 更新数据库
        updateProductInDB(product);

        // 添加到延迟删除队列
        cacheDeletionQueue.add(product.getId());
    }

    @Scheduled(fixedDelay = 100)
    public void delayedCacheDeletion() {
        String productId = cacheDeletionQueue.poll();
        if (productId != null) {
            deleteCache(productId);
        }
    }

    // ... 其他方法 ...
}

当然,这里使用@Scheduled也并不完美,一个是它是基于JVM内存的,一旦应用重启,任务就丢失了,还有就是他这里使用的是阻塞队列,如果任务太多也会有问题。

这里反正可以用MQ或者Redisson来实现延迟队列更完美一点,或者给予binlog监听做第二次删除也可以。