【Java】缓存击穿解决方案


个人秋招总结
网关项目推荐
C端项目

什么是SingleFlight?

SingleFlight是go语言中sync包中的一个东西。它用于确保在并发环境下某个操作(例如,函数调用)即使被多个goroutine同时请求,也只会被执行一次。这对于防止重复的、昂贵的操作(如数据库查询、HTTP请求等)被不必要地多次执行是非常有用的。
使用 sync.SingleFlight,可以确保对于同一个键的并发请求,在缓存失效的情况下,只有一个请求会去加载数据(例如从数据库中),而其他并发的请求会等待这个加载操作完成,并共享相同的结果。这样,即便缓存失效,也不会因为大量的并发请求而对数据库或后端服务产生压力。
具体来说,当缓存失效时,第一个到达的请求会触发数据加载的操作(如数据库查询),而其他同时到达的请求会等待这个操作的完成。一旦数据被加载,它会被返回给所有等待的请求,并重新被放入缓存中。这个过程中,sync.SingleFlight 保证了数据加载函数只被调用一次,避免了不必要的重复处理。

SingleFlight主要提供以下功能:
● Do(key string, fn func() (interface{}, error)): 这是SingleFlight最核心的方法。当多个goroutine同时调用Do方法时,只有一个会真正执行传入的fn函数,其它等待这个函数执行完成。执行完成后,返回的结果和错误将会被返回给所有调用Do方法的goroutine。这里的key是用来区分不同操作的唯一标识。
● DoChan(key string, fn func() (interface{}, error)): 与Do类似,但它返回一个channel,你可以从这个channel中读取执行结果。
● Forget(key string): 这个方法用于清除SingleFlight中缓存的结果,以便于同一个key对应的函数在未来可以再次被执行。
● DupSuppressed() int64: 返回被SingleFlight机制抑制的重复调用次数。
SingleFlight的一个常见用途是缓存层,避免在缓存失效时由于缓存击穿而导致大量请求直接落到数据库。
如下是在写go语言的时候的使用SingleFight解决缓存击穿的代码。

var g singleflight.Group

func getCachedData(key string) (data interface{}, err error) {
    // 使用Do方法确保对于同一个key的请求,函数只会被执行一次
    v, err, _ := g.Do(key, func() (interface{}, error) {
        // 这里是实际的获取数据的操作,比如从数据库读取
        return getDataFromDatabase(key)
    })
    return v, err
}

func getDataFromDatabase(key string) (interface{}, error) {
    // 模拟数据库操作
    // ...
    return data, nil
}

优化

SingleFlight 是一种用于减少重复工作的工具,特别是在并发编程中处理类似缓存击穿这样的问题时。尽管它非常有用,但也有一些潜在的缺点和限制:

缺点

  1. 资源锁定:如果用于一个长时间运行的操作,SingleFlight 会阻止其他所有相关的请求,直到这个操作完成。这可能导致长时间的等待,特别是在操作非常耗时的情况下。
  2. 错误传播:如果共享的操作因为某些原因失败了,这个错误会被传播给所有等待的请求。在某些情况下,单独重试可能更合适。
  3. 内存压力:在高并发情况下,如果许多不同的键被请求,SingleFlight 结构可能占用大量内存。
  4. 不适合高变动数据:对于频繁变化的数据,使用 SingleFlight 可能不太有效,因为一旦数据被缓存,就需要等待旧数据失效才能获取新数据。
  5. 分布式锁:缺乏对分布式场景下的解决方案

优化策略

  1. 设置超时:为 SingleFlight 中的操作设置合理的超时时间,可以防止一个慢操作阻塞其他请求过长时间。
  2. 错误重试机制:对于某些操作,特别是网络请求等,实现自动重试逻辑可能会有帮助,而不是直接将一个失败共享给所有请求。
  3. 限制并发数量:可以对 SingleFlight 正在进行的操作数量设置上限,以减少内存压力。
  4. 数据版本控制:对于频繁变化的数据,可以结合数据版本控制,确保即使在数据更新的时候也能获取到最新的数据。

对上面的缺点进行优化,得到如下Java代码。

Java实现SingleFlight

package blossom.project.towelove.common.utils;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;

/**
 * @author: ZhangBlossom
 * @date: 2024/1/9 22:06
 * @contact: QQ:4602197553
 * @contact: WX:qczjhczs0114
 * @blog: https://blog.csdn.net/Zhangsama1
 * @github: https://github.com/ZhangBlossom
 * @description: 对于go语言中解决缓存击穿工具SingleFlight的实现
 */
public abstract class SingleFlight<K, V> {
    private final ConcurrentHashMap<K, CompletableFuture<V>> ongoingOperations = new ConcurrentHashMap<>();

    /**
     * 版本号控制,使得每次进行更新的时候,一定都是对最新数据进行更新
     */
    private final ConcurrentHashMap<K, AtomicLong> versions = new ConcurrentHashMap<>();

    // 设置默认超时时间,例如3秒
    private final long defaultTimeout = 3 * 1000;


    protected abstract boolean acquireDistributedLock(K key);

    protected abstract void releaseDistributedLock(K key);

    /**
     * 更新指定键的版本号
     *
     * @param key 键
     */
    public void updateVersion(K key) {
        versions.compute(key, (k, version) -> {
            if (version == null) {
                return new AtomicLong(1);
            } else {
                version.incrementAndGet();
                return version;
            }
        });
    }

    /**
     * 获取指定键的当前版本号
     *
     * @param key 键
     * @return 版本号
     */
    private long getVersion(K key) {
        return versions.getOrDefault(key, new AtomicLong(0)).get();
    }

    /**
     * 确保对于同一个键,相关的操作只会被执行一次,并且其结果将被所有调用者共享.
     * 如果超时了没有complete,将会返回TimeoutException
     *
     * @param key      唯一key
     * @param function 要执行的方法函数
     * @param timeout  超时时间,单位为ms,默认3000ms=3s
     * @return
     */
    public CompletableFuture<V> doOrTimeout(K key, Function<K, V> function, long timeout, boolean useDistributedLock) {
        if (useDistributedLock && !acquireDistributedLock(key)) {
            CompletableFuture<V> future = new CompletableFuture<>();
            future.completeExceptionally(new IllegalStateException("Unable to acquire distributed lock"));
            return future;
        }

        try {
            long versionAtCallTime = getVersion(key);
            return ongoingOperations.compute(key, (k, existingFuture) -> {
                if (existingFuture == null || getVersion(k) != versionAtCallTime) {
                    CompletableFuture<V> future = new CompletableFuture<>();
                    CompletableFuture.runAsync(() -> {
                        try {
                            V result = function.apply(k);
                            future.complete(result);
                        } catch (Exception e) {
                            future.completeExceptionally(e);
                        } finally {
                            ongoingOperations.remove(k);
                        }
                    });

                    // 应用超时设置
                    return future.orTimeout(timeout, TimeUnit.MILLISECONDS);
                }
                return existingFuture;
            });
        } finally {
            if (useDistributedLock) {
                releaseDistributedLock(key);
            }
        }
    }


    /**
     * 当前方法会异步执行任务,并保证只有一个key能执行function任务,其他任务进行等待
     * 同时,如果执行失败,那么允许设定重试次数。并且再次执行function方法。
     *
     * @param key                 执行方法唯一key
     * @param function            要执行的任务
     * @param retries             重试次数
     * @param timeout             超时时间
     * @param delayBetweenRetries 重试前延迟时间
     * @return
     */
    public CompletableFuture<V> doOrRetry(K key, Function<K, V> function, int retries, long timeout,
                                          long delayBetweenRetries, boolean useDistributedLock) {
        if (useDistributedLock && !acquireDistributedLock(key)) {
            CompletableFuture<V> future = new CompletableFuture<>();
            future.completeExceptionally(new IllegalStateException("Unable to acquire distributed lock"));
            return future;
        }
        try {
            long versionAtCallTime = getVersion(key);
            return ongoingOperations.compute(key, (k, existingFuture) -> {
                if (existingFuture == null || getVersion(k) != versionAtCallTime) {
                    CompletableFuture<V> future = new CompletableFuture<>();
                    executeWithRetriesOrCompensate(future, key, function, null, retries, timeout, delayBetweenRetries
                            , versionAtCallTime);
                    return future;
                }
                return existingFuture;
            });
        } finally {
            if (useDistributedLock) {
                releaseDistributedLock(key);
            }
        }
    }

    /**
     * 当前方法会异步执行任务,并保证只有一个key能执行function任务,其他任务进行等待
     * 同时,如果执行失败,那么允许设定重试次数。并且执行compensation补偿方法。
     *
     * @param key                 执行方法唯一key
     * @param function            原有方法
     * @param compensation        补偿方法 在执行失败的时候执行
     * @param retries             重试次数
     * @param timeout             超时时间
     * @param delayBetweenRetries 重试前延迟时间
     * @return
     */
    public CompletableFuture<V> doOrCompensate(K key, Function<K, V> function, Function<K, V> compensation,
                                               int retries, long timeout, long delayBetweenRetries,
                                               boolean useDistributedLock) {
        if (useDistributedLock && !acquireDistributedLock(key)) {
            CompletableFuture<V> future = new CompletableFuture<>();
            future.completeExceptionally(new IllegalStateException("Unable to acquire distributed lock"));
            return future;
        }
        try {
            long versionAtCallTime = getVersion(key);
            return ongoingOperations.compute(key, (k, existingFuture) -> {
                if (existingFuture == null || getVersion(k) != versionAtCallTime) {
                    CompletableFuture<V> future = new CompletableFuture<>();
                    executeWithRetriesOrCompensate(future, key, function, compensation, retries, timeout,
                            delayBetweenRetries, versionAtCallTime);
                    return future;
                }
                return existingFuture;
            });
        } finally {
            if (useDistributedLock) {
                releaseDistributedLock(key);
            }
        }
    }

    /**
     * @param future
     * @param key
     * @param function
     * @param compensation
     * @param retries
     * @param timeout
     * @param delayBetweenRetries
     * @param versionAtCallTime
     */
    private void executeWithRetriesOrCompensate(CompletableFuture<V> future,
                                                K key, Function<K, V> function, Function<K, V> compensation,
                                                int retries, long timeout, long delayBetweenRetries,
                                                long versionAtCallTime) {
        CompletableFuture.runAsync(() -> {
                    try {
                        if (getVersion(key) != versionAtCallTime) {
                            throw new IllegalStateException("Data version changed");
                        }
                        V result = function.apply(key);
                        future.complete(result);
                    } catch (Exception e) {
                        if (retries > 0 && getVersion(key) == versionAtCallTime) {
                            try {
                                TimeUnit.MILLISECONDS.sleep(delayBetweenRetries);
                            } catch (InterruptedException ignored) {
                            }
                            Function<K, V> nextFunction = (compensation != null) ? compensation : function;
                            executeWithRetriesOrCompensate(future, key, nextFunction, compensation, retries - 1,
                                    timeout, delayBetweenRetries, versionAtCallTime);
                        } else {
                            future.completeExceptionally(e);
                        }
                    }
                }).orTimeout(timeout, TimeUnit.MILLISECONDS)
                .exceptionally(ex -> {
                    if (retries > 0 && ex instanceof TimeoutException && getVersion(key) == versionAtCallTime) {
                        Function<K, V> nextFunction = (compensation != null) ? compensation : function;
                        executeWithRetriesOrCompensate(future, key, nextFunction, compensation, retries - 1, timeout,
                                delayBetweenRetries, versionAtCallTime);
                    } else {
                        future.completeExceptionally(ex);
                    }
                    return null;
                });
    }


    /**
     * 提供一个方式来异步地执行操作,并返回一个 CompletableFuture,
     * 该 CompletableFuture 可以让调用者在未来某个时刻获取操作的结果
     *
     * @param key
     * @param function
     * @return
     */
    public CompletableFuture<CompletableFuture<V>> doChan(K key, Function<K, V> function) {
        return CompletableFuture.completedFuture(ongoingOperations.computeIfAbsent(key, k -> {
            CompletableFuture<V> future = new CompletableFuture<>();
            CompletableFuture.runAsync(() -> {
                try {
                    V result = function.apply(k);
                    future.complete(result);
                } catch (Exception e) {
                    future.completeExceptionally(e);
                } finally {
                    ongoingOperations.remove(k);
                }
            });
            return future;
        }));
    }

    /**
     * 从 ongoingOperations 映射中移除了给定的键
     *
     * @param key
     */
    public void forget(K key) {
        ongoingOperations.remove(key);
    }


}


/**
 * 假设一个基于Redis的SingleFlight分布式锁实现
 * 从而使得SingleFlight支持分布式锁
 * @param <K>
 * @param <V>
 */
class RedisSingleFlight<K, V> extends SingleFlight<K, V> {
    // Redis 或其他分布式锁机制的实现

    @Override
    protected boolean acquireDistributedLock(K key) {
        return false;
    }

    @Override
    protected void releaseDistributedLock(K key) {

    }

    // 如果需要,可以添加特定于 Redis 的其他方法或逻辑
}
<think> 我们正在讨论Java缓存击穿解决方案。根据引用内容,缓存击穿是指高并发的热点key在缓存过期瞬间,大量请求直接访问数据库,导致数据库压力剧增的问题。结合引用,Java中的解决方案主要有互斥锁和逻辑过期两种,同时可以结合热点探测和限流等策略。 引用[1]提到缓存击穿的定义和解决方案,包括互斥锁和逻辑过期。 引用[2]强调了逻辑过期的思路:不设置过期时间,避免缓存击穿,但需要处理内存占用问题。 引用[3]展示了在Java中如何用互斥锁和逻辑过期解决缓存击穿,并提供了两种方案的对比。 引用[4]提到了SingleFlight模式(类似互斥锁)的优缺点和优化策略,如设置超时、错误重试等。 因此,针对Java解决缓存击穿问题,我们可以整理如下: 方案一:互斥锁(分布式锁) 核心思想:当缓存失效时,通过分布式锁(如Redis的setnx命令)保证只有一个线程去查询数据库并重建缓存,其他线程等待或重试。 实现步骤: 1. 尝试从缓存获取数据 2. 缓存不存在,尝试获取锁(设置一个锁的key,并设置过期时间防止死锁) 3. 获取锁成功后,再次检查缓存(双重检测,因为可能已经被其他线程重建),然后查询数据库,写入缓存,释放锁 4. 未获取锁的线程休眠一段时间后重试,或者直接返回默认值(根据业务) 优点:保证数据库不会同时被大量请求访问,安全性高。 缺点:等待的线程会有一定的延迟,且如果获取锁的线程重建缓存失败,可能导致其他线程一直重试(需要设置锁的超时时间)。 优化点(引用[4]): - 设置锁的超时时间,避免死锁。 - 引入重试机制,但限制重试次数。 - 对于长时间运行的操作,考虑使用异步方式,避免长时间阻塞。 方案二:逻辑过期(永不过期) 核心思想:缓存中不设置过期时间,而是将过期时间字段存储在数据的逻辑中(例如在value中增加一个expire字段)。当发现数据逻辑过期时,使用独立的线程(如异步线程或定时任务)去更新缓存,或者使用互斥锁让一个线程去更新,其他线程返回旧数据。 实现步骤: 1. 缓存中存储的数据结构为:{value: 实际数据, expire: 过期时间戳} 2. 读取缓存时,判断当前时间是否超过expire,如果没有则直接返回value 3. 如果已过期,则尝试获取锁,获取锁的线程开启新线程去更新缓存(异步更新),自己先返回旧数据;其他线程直接返回旧数据(或者也尝试获取锁,但未获取到则直接返回旧数据) 优点:避免缓存失效瞬间的穿透,用户基本无感知。 缺点:数据不一致时间窗口(在更新缓存期间,用户读到的是旧数据);内存占用可能增加(需要定期清理不常用的key)。 注意:引用[2]提到,不设置过期时间,然后采用逻辑过期,可以避免缓存击穿。 方案三:热点数据探测与预热 监控热点key(如使用Redis的hotkeys命令或监控访问频率),在热点key即将过期前自动刷新缓存(延长过期时间或重新加载)。对于秒杀类活动,提前将数据加载到缓存。 方案四:限流降级 在缓存层或应用层对热点key的请求进行限流(如使用令牌桶、漏桶算法),当请求超过阈值时,部分请求直接降级(返回默认值、错误页面等),保护数据库。 组合策略: 根据引用[3]的建议,可以组合使用互斥锁和逻辑过期: - 对于核心且实时性要求高的数据,使用逻辑过期+异步更新。 - 对于非核心数据,使用互斥锁。 同时,监控缓存命中率和数据库压力,设置告警,当缓存失效时动态调整策略。 伪代码示例(互斥锁): 引用[3]中ShopServiceImpl.java提供了互斥锁的实现思路,我们可以用Java代码模拟: ```java public String getDataWithLock(String key) { // 1. 从缓存中获取数据 String data = redisTemplate.opsForValue().get(key); if (data != null) { return data; } // 2. 缓存未命中,尝试获取锁 String lockKey = "lock:" + key; String lockValue = UUID.randomUUID().toString(); // 使用UUID作为锁的值,防止误删 Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS); // 设置锁超时10秒 try { if (isLock != null && isLock) { // 3. 获取锁成功,再次检查缓存(双重检测) data = redisTemplate.opsForValue().get(key); if (data != null) { return data; } // 4. 查询数据库 data = db.query(key); // 5. 写入缓存,设置过期时间(例如5分钟) redisTemplate.opsForValue().set(key, data, 5, TimeUnit.MINUTES); } else { // 6. 未获取到锁,等待一段时间后重试 Thread.sleep(50); return getDataWithLock(key); // 递归重试,注意控制递归深度或改为循环 } } finally { // 7. 释放锁,使用Lua脚本保证原子性(判断锁的值是否为自己设置的,避免删除其他线程的锁) String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(lockKey), lockValue); } return data; } ``` 伪代码示例(逻辑过期): 假设缓存数据类为: ```java class CacheData { private Object data; // 实际数据 private long expireTime; // 过期时间(时间戳) // 构造方法和getter/setter } ``` 读取数据时: ```java public Object getDataWithLogicalExpire(String key) { // 1. 从缓存中获取CacheData对象 CacheData cacheData = (CacheData) redisTemplate.opsForValue().get(key); if (cacheData == null) { // 缓存中连逻辑过期结构都没有,说明是首次访问或已被清除,可以尝试用互斥锁加载 return loadDataFromDB(key); } // 2. 检查是否逻辑过期 long current = System.currentTimeMillis(); if (cacheData.getExpireTime() >= current) { // 未过期 return cacheData.getData(); } // 3. 已逻辑过期,尝试获取锁更新缓存 String lockKey = "lock:" + key; String lockValue = UUID.randomUUID().toString(); Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS); if (isLock != null && isLock) { // 4. 获取锁成功,开启新线程更新缓存(避免阻塞当前请求) executorService.execute(() -> { try { // 查询数据库 Object newData = db.query(key); // 设置新的过期时间(比如30分钟后过期) CacheData newCacheData = new CacheData(newData, System.currentTimeMillis() + 30 * 60 * 1000); redisTemplate.opsForValue().set(key, newCacheData); } finally { // 释放锁 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockKey), lockValue); } }); } // 5. 不管有没有获取锁,都返回旧数据 return cacheData.getData(); } ``` 注意:逻辑过期方案中,获取锁失败就直接返回旧数据,因为此时有其他线程正在更新。 两种方案对比(引用[3]): 互斥锁: 优点:强一致性,保证数据最新。 缺点:存在等待,性能较低。 逻辑过期: 优点:高可用,性能好。 缺点:数据不一致(在更新期间)。 因此,选择方案需要根据业务场景。例如,对于商品详情页这种对实时性要求不高的场景,可以用逻辑过期;对于库存扣减等实时性要求高的场景,用互斥锁。 此外,引用[4]提到的SingleFlight模式在Go语言中常见,Java中可以使用ConcurrentHashMap+Future的方式实现类似效果:将多个相同key的请求合并成一个,避免重复查询数据库。但需要注意内存占用和错误传播问题。 总结Java解决缓存击穿方案: 1. 互斥锁(分布式锁)方案:适合对数据一致性要求高的场景。 2. 逻辑过期方案:适合对性能要求高且能容忍短暂数据不一致的场景。 3. 结合热点探测、缓存预热、限流降级等策略,构建更健壮的缓存系统。 相关问题: 1. 互斥锁方案中如何避免死锁和锁超时问题? 2. 逻辑过期方案中,如何减少数据不一致的时间窗口? 3. 如何实现热点key的自动识别与动态缓存策略? </think>以下是使用 Java 解决缓存击穿问题的详细方案,结合引用内容整理: --- ### 一、缓存击穿Java 解决方案 #### 方案 1:基于互斥锁(分布式锁) ```java public class CacheService { private final RedisTemplate<String, Object> redisTemplate; private final DatabaseService databaseService; public Object getDataWithLock(String key) { // 1. 尝试从缓存获取 Object data = redisTemplate.opsForValue().get(key); if (data != null) return data; // 2. 尝试获取分布式锁 String lockKey = "lock:" + key; boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS); // 锁超时5秒 try { if (locked) { // 3. 双重检查缓存(防止重复查询) data = redisTemplate.opsForValue().get(key); if (data != null) return data; // 4. 查询数据库并写入缓存 data = databaseService.queryFromDB(key); redisTemplate.opsForValue().set(key, data, 300, TimeUnit.SECONDS); } else { // 5. 未获锁时短暂等待后重试 Thread.sleep(100); return getDataWithLock(key); // 递归重试 } } finally { if (locked) redisTemplate.delete(lockKey); // 释放锁 } return data; } } ``` ✅ **优点**: - 确保单线程重建缓存,防止数据库被打垮[^1][^3] ⚠️ **优化点**: - 设置合理的锁超时时间(避免死锁) - 增加重试次数限制(防止无限递归) #### 方案 2:基于逻辑过期(逻辑永不过期) ```java public class CacheService { // 数据包装类(包含逻辑过期时间) @Data static class CacheWrapper { private Object data; private long expireTime; // 逻辑过期时间戳 } public Object getDataWithLogicalExpire(String key) { // 1. 从缓存获取包装对象 CacheWrapper wrapper = (CacheWrapper) redisTemplate.opsForValue().get(key); if (wrapper == null) { // 首次加载(需加锁) return loadData(key); } // 2. 检查逻辑过期 if (System.currentTimeMillis() > wrapper.getExpireTime()) { // 3. 异步更新缓存 executorService.submit(() -> refreshCache(key)); } return wrapper.getData(); // 返回旧数据 } private void refreshCache(String key) { // 后台线程更新数据并设置新过期时间 Object newData = databaseService.queryFromDB(key); CacheWrapper newWrapper = new CacheWrapper(); newWrapper.setData(newData); newWrapper.setExpireTime(System.currentTimeMillis() + 300_000); // 5分钟后过期 redisTemplate.opsForValue().set(key, newWrapper); } } ``` ✅ **优点**: - 零等待时间,避免线程阻塞[^2] - 彻底解决缓存失效问题 ⚠️ **注意事项**: - 需监控内存占用(无自动过期机制)[^2] - 需处理数据一致性(可能返回旧数据) --- ### 二、进阶优化策略 #### 1. 组合方案(互斥锁 + 逻辑过期) ```java // 核心热点 key 使用逻辑过期 private static final Set<String> HOT_KEYS = Set.of("hot_item_001", "hot_item_002"); public Object getDataSmart(String key) { if (HOT_KEYS.contains(key)) { return getDataWithLogicalExpire(key); // 热点 key 逻辑过期 } return getDataWithLock(key); // 普通 key 互斥锁 } ``` #### 2. 热点 Key 动态识别 ```java // 监控访问频率(示例) public void monitorHotKeys() { Map<String, Integer> accessCount = new ConcurrentHashMap<>(); // 每次访问计数 public Object getData(String key) { accessCount.compute(key, (k, v) -> v == null ? 1 : v + 1); // ... 正常逻辑 } // 定时任务识别热点 key(每5分钟) @Scheduled(fixedRate = 300_000) public void detectHotKeys() { accessCount.entrySet().stream() .filter(entry -> entry.getValue() > 1000) // 阈值 .forEach(entry -> HOT_KEYS.add(entry.getKey())); accessCount.clear(); } } ``` #### 3. 限流降级保护 ```java // 使用 Guava RateLimiter 限流 private final RateLimiter limiter = RateLimiter.create(100); // 100 QPS public Object getDataWithLimit(String key) { if (!limiter.tryAcquire()) { return defaultData; // 限流时返回兜底数据 } return getDataSmart(key); } ``` --- ### 三、方案对比与选型 | **方案** | **适用场景** | **优点** | **缺点** | |-------------------|---------------------------|--------------------------|--------------------------| | 互斥锁 | 强一致性场景(如库存扣减) | 数据强一致 | 线程阻塞风险[^4] | | 逻辑过期 | 高并发读场景(如商品详情) | 零等待、高性能 | 数据短暂不一致 | | 热点 Key 动态识别 | 突发流量场景(如热搜话题) | 自适应业务变化 | 实现复杂度高 | | 限流降级 | 系统保护场景 | 防止雪崩效应 | 部分请求降级 | > 选型建议:核心业务用互斥锁 + 非核心业务用逻辑过期,结合热点监控和限流[^3] --- ### 四、生产环境注意事项 1. **锁超时设置**:必须设置锁超时(参考值:业务查询时间的 2~3 倍) 2. **双重检查锁定**:获取锁后需再次检查缓存(避免重复更新) 3. **异步更新重试**:逻辑过期方案需添加失败重试机制 4. **监控告警**:监控缓存命中率、锁等待时间、数据库 QPS ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZhangBlossom

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值