1、缓存穿透

解决方案:

1.1、布隆过滤器

原理:核心是一个bitmap(位数组),初始值都是0,用k个hash函数对某个key进行哈希,哈希出来的值对数组长度取模,取模出来的值就是bitmap位数组的下标,将这个下标改为1。例如有三个hash函数,其中一个hash函数对某个key哈希出来的值是6354719,然后对数组长度取模,比如数组长度为20,则6354719%20=19,则将bitmap位数组的19下标改为1。

容错率:实则就是误判的概率。跟数组长度、hash函数的个数有关,必需配合使用。容错率越小,创建数组长度越大,性能越低。误判解释如下几个图所示:

redis hincrby竞争问题_分布式


redis hincrby竞争问题_redis hincrby竞争问题_02


redis hincrby竞争问题_redis hincrby竞争问题_03


解释:当往布隆过滤器中添加了1-20的数字时,布隆过滤器中黄色的下标是已经被改为1了,此时位数组长度差不多被改满了。而输入一个不存在布隆过滤器中的数据,比如说111时,它却说可能存在,所以此时出现了误判。

维护:添加表记录的时候往布隆过滤器添加一份;表中如果删除了大量的数据应重建布隆过滤器。

优点:
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和查询时间都是常数({\displaystyle O(k)} )。另外,散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。

缺点:
但是布隆过滤器的缺点和优点一样明显。误判率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。

实现:

package com.luban.testcache.filter;


import com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Scope;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;

import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
import java.util.List;


@ConfigurationProperties("bloom.filter")
@Component
public class RedisBloomFilter {

    //预计插入量
    private long expectedInsertions;

    //可接受的错误率
    private double fpp;

    @Autowired
    private RedisTemplate redisTemplate;


    //bit数组长度
    private long numBits;
    //hash函数数量
    private int numHashFunctions ;

    public long getExpectedInsertions() {
        return expectedInsertions;
    }

    public void setExpectedInsertions(long expectedInsertions) {
        this.expectedInsertions = expectedInsertions;
    }

    public void setFpp(double fpp) {
        this.fpp = fpp;
    }

    public double getFpp() {
        return fpp;
    }

    @PostConstruct
    public void init(){
        this.numBits = optimalNumOfBits(expectedInsertions, fpp);
        this.numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    }

    //计算hash函数个数
    private int optimalNumOfHashFunctions(long n, long m) {
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
    }

    //计算bit数组长度
    private long optimalNumOfBits(long n, double p) {
        if (p == 0) {
            p = Double.MIN_VALUE;
        }
        return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
    }

    /**
     * 判断keys是否存在于集合
     */
    public boolean isExist(String key) {
        long[] indexs = getIndexs(key);
        List list = redisTemplate.executePipelined(new RedisCallback<Object>() {

            @Nullable
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                redisConnection.openPipeline();
                for (long index : indexs) {
                    redisConnection.getBit("bf:taibai".getBytes(), index);
                }
                redisConnection.close();
                return null;
            }
        });
        return !list.contains(false);
    }

    /**
     * 将key存入redis bitmap
     */
    public void put(String key) {
        long[] indexs = getIndexs(key);
        redisTemplate.executePipelined(new RedisCallback<Object>() {

            @Nullable
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                redisConnection.openPipeline();
                for (long index : indexs) {
                    redisConnection.setBit("bf:taibai".getBytes(),index,true);
                }
                redisConnection.close();
                return null;
            }
        });
    }

    /**
     * 根据key获取bitmap下标
     */
    private long[] getIndexs(String key) {
        long hash1 = hash(key);
        long hash2 = hash1 >>> 16;
        long[] result = new long[numHashFunctions];
        for (int i = 0; i < numHashFunctions; i++) {
            long combinedHash = hash1 + i * hash2;
            if (combinedHash < 0) {
                combinedHash = ~combinedHash;
            }
            result[i] = combinedHash % numBits;
        }
        return result;
    }

    /**
     * 获取一个hash值
     */
    private long hash(String key) {
        Charset charset = Charset.forName("UTF-8");
        return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
    }
}

使用:容器启动时将数据库中表数据put到布隆过滤器中

@PostConstruct
    public void init(){
        List<Order> orders = orderService.selectOrderyAll();
        for (Order order : orders) {
            redisBloomFilter.put(String.valueOf(order.getId()));
        }
    }

1.2、将一些查询不到的值设为null。

2、缓存击穿

2.1、解决方案:使用分布式锁。

/**
     * 分布式锁解决缓存击穿
     * @param key 键
     * @return
     */
    public String getKey(Integer key) {
        String value = (String)redisTemplate.opsForValue().get(key+"");
        if(value == null){
            String mutexKey = "mutex:key:"+key; //设置分布式锁的key
            if(redisTemplate.opsForValue().setIfAbsent(mutexKey,"1",180, TimeUnit.SECONDS)){ //给这个key上一把分布式锁,ex表示只有一个线程能执行,过期时间为180秒
                // 从数据库中查出该数据放入redis
                value = userDao.getUserName(key);
                redisTemplate.opsForValue().set(key+"",value);
                // 释放锁
                redisTemplate.delete(mutexKey);
            }else{
                // 其他的线程休息100毫秒后重试
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                getKey(key);
            }}
        return value;
    }

redis.set(mutexKey,“1”,“ex 180”,“nx”)则对应redis中的SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]命令,例子如下:

127.0.0.1:6379> set a 1 ex 10 nx
OK
127.0.0.1:6379> ttl a
8

测试一下:第一次查询,输出了sql语句。

redis hincrby竞争问题_缓存_04


redis hincrby竞争问题_redis hincrby竞争问题_05


看看redis

redis hincrby竞争问题_缓存_06

第二次再查询,还是这个样子,什么都没输出,查redis。

redis hincrby竞争问题_分布式_07


搞定。

3、缓存雪崩

原因:redis中key大面积同时失效,导致大量并发请求都打到了数据库上。

解决方案:
1、设置redis中的key随机的过期时间。
2、配置redis高可用集群(主从复制、读写分离、哨兵、集群等)。