logo头像
Snippet 博客主题

如何实现缓存击穿,只允许一条线程去DB更新数据

本文于 869 天之前发表,文中内容可能已经过时。

场景

使用redis控制缓存的时候,弊端是太依赖于网络与redis服务的稳定性。在高并发的场景下,如果某个KEY过期了,会有很多的GET该KEY的请求,直接会去查询DB,甚至导致DB瘫痪,那么有啥解决方案呢?

解决方案

利用互斥锁,只允许一个线程去DB查询,其他线程等待一段时间重试,当查询DB反馈结果后重新设置到缓存中,那么其他线程直接走缓存就可以了。这里的锁,可以选分布式锁,比如zookeeper锁,JVM锁,redis的setNx互斥锁。

  1. ZooKeeper锁的弊端是,堵塞锁,即只要有线程已经获得锁,其他想获取锁的线程都会等待锁释放,影响QPS。
  2. JVM锁,可以用Lock,Synchronized。推荐的用法是《JAVA并发编程的艺术》提到的concurrentHashMap与futureTask,代码如下
    `
    public class CacheUtil {

    private final ConcurrentMap<Object, Future> taskCache =

         new ConcurrentHashMap<Object, Future<String>>();
    

    private String executionTask(final String taskName)

         throws ExecutionException, InterruptedException {
     while (true) {
         Future<String> future = taskCache.get(taskName);
         if (future == null) {
             Callable<String> task = new Callable<String>() {
                 public String call() throws InterruptedException {
                     return taskName;//loadDb
                 }
             };
             FutureTask<String> futureTask = new FutureTask<String>(task);
             future = taskCache.putIfAbsent(taskName, futureTask);
             if (future == null) {
                 future = futureTask;
                 futureTask.run();
             }
         }
         try {
             return future.get();
         } catch (CancellationException e) {
             taskCache.remove(taskName, future);
         }
     }
    

    }

}


弊端就是只能限制同个进程的缓存更新。

3. redis分布式锁。值得注意的是低版本的redis,setNx与expire是二步操作,不能保证原子性,下面提供一个高版本的redis,setNx可以设置过期时间的。

public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
String keynx = key.concat(“:nx”);
if (redis.setnx(keynx, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(keynx);
} else {
//这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
`