缓存穿透

​ 缓存穿透是指用户查询数据时,数据库和缓存中都没有数据。导致了查询请求直接绕过缓存,直接穿透到数据库。

解决方案一:缓存空值

即如果查询id为null,则将null直接放入缓存

    /**
     * 解决缓存穿透
     * @return
     */
    public User getUser1(String userId) {
        //从缓存中获取user信息
        User user = (User) redisTemplate.opsForValue().get(userId);

        if(user == null) {
            //如果缓存数据为空,从数据库中获取user信息
            user = lUserMapper.getUserByUserId(userId);

            if(user == null) {
                //如果数据库中数据为空,则存入一个空值,设置短时间内过期,防止缓存穿透
                redisTemplate.opsForValue().set(userId,null,3, TimeUnit.MINUTES);
            }else {
                //将数据写入缓存
                redisTemplate.opsForValue().set(userId,user);
            }
        }
        return user;
    }

方案二:布隆过滤器

布隆过滤器基本定义及原理

​ 布隆过滤器本质上是一种数据结构,其作用就是用来快速判断某个数据一定不存在或可能存在

​ 其原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射为一个位数组中的K个位置,并把它们置为1,检索时,只需要查询对应位置是否都为1,就能确认该元素可能存在。

引入依赖

        <!--guava布隆过滤器-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>22.0</version>
        </dependency>

BloomFilterHelper

import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;

/**
 * bloom
 */
public class BloomFilterHelper<T> {

    private int numHashFunctions;

    private int bitSize;

    private Funnel<T> funnel;

    public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
        Preconditions.checkArgument(funnel != null, "funnel不能为空");
        this.funnel = funnel;
        // 计算bit数组长度
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        // 计算hash方法执行次数
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
    }

    public int[] murmurHashOffset(T value) {
        int[] offset = new int[numHashFunctions];

        long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
        int hash1 = (int) hash64;
        int hash2 = (int) (hash64 >>> 32);
        for (int i = 1; i <= numHashFunctions; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
                nextHash = ~nextHash;
            }
            offset[i - 1] = nextHash % bitSize;
        }

        return offset;
    }

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

    /**
     * 计算hash方法执行次数
     */
    private int optimalNumOfHashFunctions(long n, long m) {
        int countOfHash = Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
        return countOfHash;
    }
}

添加判断值工具类

import com.google.common.base.Preconditions;
import com.scholarship.common.config.BloomFilterHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

/**
 * 布隆过滤器工具类
 */
@Component
public class BloomUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据给定的布隆过滤器添加值
     * @param bloomFilterHelper
     * @param key
     * @param value
     */
    public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            redisTemplate.opsForValue().setBit(key, i, true);
        }
    }

    /**
     * 根据给定的布隆过滤器判断值是否存在
     * @param bloomFilterHelper
     * @param key
     * @param value
     * @return
     */
    public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            if (!redisTemplate.opsForValue().getBit(key, i)) {
                return false;
            }
        }

        return true;
    }
}

项目启动类部分

    /**
     * 初始化布隆过滤器,放入spring容器
     * @return
     */
    @Bean
    public BloomFilterHelper<String> initBloomFilterHelper() {
        //设置预计插入量为1000,可接受错误率为0.01
        return new BloomFilterHelper<>(
                (Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8).putString(from, Charsets.UTF_8),
                1000, 0.01);
    }

实现代码

当添加一个新的用户信息或者启动项目时,可以先将已存在的用户id保存在布隆过滤器中

List<User> userList = lUserMapper.listUser();
for(User user : userList) {
    bloomUtil.addByBloomFilter(bloomFilterHelper,"bloomUser",user.getUserId());
}

获取该用户id的用户信息前先进行判断是否存在

    /**
     * 解决缓存穿透
     * @return
     */
    public User getUser4(String userId) {
        //先在布隆过滤器中查询userId是否存在
        Boolean flag = bloomUtil.includeByBloomFilter(bloomFilterHelper,"bloomUser",userId);
        if(flag == false) {
            return null;
        }
        
        //从缓存中获取user信息
        User user = (User) redisTemplate.opsForValue().get(userId);

        if(user == null) {
            //如果缓存数据为空,从数据库中获取user信息
            user = lUserMapper.getUserByUserId(userId);

            //将数据写入缓存
            redisTemplate.opsForValue().set(userId,user);
        }
        return user;
    }

缓存雪崩

​ 缓存雪崩指的是缓存中大量数据集中在同一时间失效,造成大量请求同时穿透到数据库导致系统崩溃

解决方案

​ 设置不同的过期时间,使缓存失效时间散开

    /**
     * 解决缓存雪崩
     * @return
     */
    public User getUser2(String userId) {
        //从缓存中获取user信息
        User user = (User) redisTemplate.opsForValue().get(userId);

        if(user == null) {
            //如果缓存数据为空,从数据库中获取user信息
            user = lUserMapper.getUserByUserId(userId);

            if(user == null) {
                redisTemplate.opsForValue().set(userId,null,3, TimeUnit.MINUTES);
            }else {
                //设置随机过期时间,将数据写入缓存,防止缓存雪崩
                long mins = random.nextInt(60) + 60;
                redisTemplate.opsForValue().set(userId, user, mins, TimeUnit.MINUTES);
            }
        }
        return user;
    }

缓存击穿

​ 缓存击穿是缓存时间到期,使得缓存中没有数据但数据库存在数据。此时大量并发请求涌入数据库,导致数据库查询压力增大。

解决方案一:不设置失效时间

解决方案二:加锁

​ 加锁,保证同一时刻,只能有一个线程去访问数据库,高并发下还需要进行双重检查,证明缓存中真的不存在数据

    /**
     * 解决缓存击穿
     * @return
     */
    public User getUser3(String userId) {
        //从缓存中获取user信息
        User user = (User) redisTemplate.opsForValue().get(userId);

        //双重检测
        if(user == null) {
            //如果缓存数据为空,读取数据库的过程加锁
            synchronized(this) {
                //获取到锁后要再次判断缓存中是否存在数据
                //防止缓存击穿,大量请求访问数据库
                user = (User) redisTemplate.opsForValue().get(userId);

                if(user == null) {
                    //如果缓存数据还为空,从数据库中获取user信息
                    user = lUserMapper.getUserByUserId(userId);

                    if(user == null) {
                        redisTemplate.opsForValue().set(userId,null,3, TimeUnit.MINUTES);
                    }else {
                        long mins = random.nextInt(60) + 60;
                        redisTemplate.opsForValue().set(userId, user, mins, TimeUnit.MINUTES);
                    }
                }
            }
        }
        return user;
    }
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐