redis缓存穿透解决方案

什么是缓存穿透

场景如下图所示:

   

        黑客每次故意查询一个在缓存内必然不存在的数据,导致每次请求都要去数据库中去查询,这样缓存就失去了意义。如果几十万的大请求越过缓存,直接怼到数据库,数据库很可能挂掉,造成整体服务荡掉,这就是缓存穿透。

解决方案

    在这里我们给出三套解决方案,大家根据项目中的实际情况,选择使用。

1、使用互斥锁

    该方法是比较普遍的做法,即,在根据key获得的value值为空时,先锁上,再从数据库加载,加载完毕,释放锁。若其他线程发现获取锁失败,则睡眠50ms后重试。

至于锁的类型,单机环境用并发包的Lock类型就行,集群环境则使用分布式锁( redis的setnx)

集群环境的redis的代码如下所示:

#伪代码
String get(String key) {  

   String value = redis.get(key);  

   if (value  == null) {  

    if (redis.setnx(key_mutex, "1")) {  

        // 3 min timeout to avoid mutex holder crash  

        redis.expire(key_mutex, 3 * 60)  

        value = db.get(key);  

        redis.set(key, value);  

        redis.delete(key_mutex);  

    } else {  

        //其他线程休息50毫秒后重试  

        Thread.sleep(50);  

        get(key);  

    }  

  }  

}  

优点

  1. 思路简单

  2. 保证一致性

缺点

  1. 代码复杂度增大

  2. 存在死锁的风险

  3. 小型项目可用        

 

2、异步构建缓存

     在这种方案下,构建缓存采取异步策略,会从线程池中取线程来异步构建缓存,从而不会让所有的请求直接怼到数据库上。该方案redis自己维护一个timeout,当timeout小于System.currentTimeMillis()时,则进行缓存更新,否则直接返回value值。

集群环境的redis代码如下所示:

#伪代码
String get(final String key) {  

        V v = redis.get(key);  

        String value = v.getValue();  

        long timeout = v.getTimeout();  

        if (v.timeout <= System.currentTimeMillis()) {  

            // 异步更新缓存
            threadPool.execute(new Runnable() {  

                public void run() {  

                    String keyMutex = "mutex:" + key;  

                    if (redis.setnx(keyMutex, "1")) {  

                        // 3 min timeout to avoid mutex holder crash  

                        redis.expire(keyMutex, 3 * 60);  

                        String dbValue = db.get(key);  

                        redis.set(key, dbValue);  

                        redis.delete(keyMutex);  

                    }  

                }  

            });  

        }  

        return value;  

    }

优点

  1. 性价最佳,用户无需等待

缺点

  1. 无法保证缓存一致性

  2. 需要进行缓存预热操作,将数据提前放入缓存

 

3、布隆过滤器(推荐)

        布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。因此非常适合解决缓存穿透问题,将已存在的数据放到布隆过滤器中,当黑客访问不存在的缓存时,迅速返回null,避免缓存及DB挂掉。

  guava包已经帮我们实现布隆过滤器的java版本了,可以直接拿来用。

  springboot2.x以上版本,已经帮我们导入guava包的依赖了。

  guava包的依赖是:

<dependencies>  

        <dependency>  

            <groupId>com.google.guava</groupId>  

            <artifactId>guava</artifactId>  

            <version>22.0</version>  

        </dependency>  

    </dependencies>

实际使用步骤

  1. 将需要缓存的key,先存入布隆过滤器中,代码如下所示:

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import com.orange.shop.bo.Order;
import com.orange.shop.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import java.util.List;

/**
 * @ClassName: LegalIdsBloomFilter
 * @Auther: chw
 * @Description: 布隆过滤器
 * @Vsersion: 1.0
 */

@Configuration
@EnableScheduling //开启定时器
@Slf4j
public class LegalIdsBloomFilter {

    @Autowired
    private OrderMapper orderMapper;

    //容量
    private static int capacity=1000000

    //创建一个能够容纳100万个元素且容错率默认为0.03布隆过滤器
    private static final BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), capacity);


    /**
     * 开启定时任务,将合法id存入过滤器
     * 每5分钟执行一次
     */
    @Scheduled(cron = "0 0/5 * * * ?")
    public void initLegalIdsBloomFilter() {
        log.info("................定时调度任务-配置布隆过滤器...............");
        List<Order> orders = orderMapper.selectAll();
        orders.forEach(order -> bloomFilter.put(order.getId()));
    }


    /**
     * 布隆过滤器匹配
     * @param id
     * @return
     */
    public boolean checkLegalId(String id) {
        return bloomFilter.mightContain(id);
    }

}

   2. 布隆过滤器先匹配传来的key,不存在直接返回null;如果存在,先读取缓存,缓存没有,再读取db,这样可大幅度阻挡不合法key侵入,从而保护缓存和db,代码如下所示:

   

   /**
     * 测试缓存穿透-布隆过滤器
     * @param id
     * @param threadName
     * @return
     */
    @Override
    public List<Order> getObjectByBloom(String id, String threadName) {

        //返回结果
        List<Order> orderList = new ArrayList<>();

        //判断是否为合法id
        if (!legalIdsBloomFilter.checkLegalId(id)) {
            //不合法id,直接返回为null
            log.info("当前线程:[{}],return by bloomFilter", threadName);
            return null;
        } else {
            //从缓存中获取
            String redisKey = id;
            String redisValue = redisTemplate.opsForValue().get(redisKey);
            if (StringUtils.isNotBlank(redisValue)) {
                log.info("当前线程:[{}],from Redis", threadName);
                orderList = JSON.parseArray(redisValue, Order.class);
            } else {
                //查询数据库
                log.info("当前线程:[{}],from DB", threadName);
                Order order = orderMapper.selectByPrimaryKey(id);
                orderList.add(order);
                //存入redis
                redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(orderList), 3L, TimeUnit.MINUTES);
            }
        }
        return orderList;

    }

优点

  1. 思路简单

  2. 保证一致性

  3. 性能强

  4. 适合大型项目使用

缺点

  1. 代码复杂度增大

  2. 需要维护一个布隆过滤器,来存放缓存的key

  3. 布隆过滤器不支持删值操作

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值