布隆过滤器解决缓存穿透-Springboot代码示例

缓存击穿

缓存穿透:指查询一个不存在的数据,缓存中没有相应的记录,每次请求都会去数据库查询,造成数据库负担加重。

解决方案一

布隆过滤器

布隆过滤器(Bloom Filter)是一个节省空间的概率型数据结构,用于判断一个元素是否在集合中。以下是它的主要原理:

  1. 基本组成:
  • 一个长度为 m 的位数组(初始全部为0)
  • k 个不同的哈希函数
  1. 工作原理:

插入元素:

1. 当插入一个元素时,使用 k 个哈希函数计算出 k 个哈希值
2. 将位数组中对应的 k 个位置都设为 1

查询元素:

1. 同样使用 k 个哈希函数计算出 k 个哈希值
2. 检查位数组中对应的 k 个位置
3. 如果有任何一个位置为 0,则该元素一定不在集合中
4. 如果所有位置都为 1,则该元素可能在集合中(存在误判)
  1. 特点:
  • 空间效率高
  • 不支持删除操作
  • 有一定的误判率(False Positive)
  • 不会漏判(False Negative)
  1. 示例代码:
public class BloomFilter {
    private BitSet bitSet;
    private int size;
    private int hashFunctions;
    
    public BloomFilter(int size, int hashFunctions) {
        this.size = size;
        this.hashFunctions = hashFunctions;
        bitSet = new BitSet(size);
    }
    
    public void add(String element) {
        for(int i = 0; i < hashFunctions; i++) {
            bitSet.set(hash(element, i));
        }
    }
    
    public boolean mightContain(String element) {
        for(int i = 0; i < hashFunctions; i++) {
            if(!bitSet.get(hash(element, i))) {
                return false;
            }
        }
        return true;
    }
    
    private int hash(String element, int seed) {
        // 实现哈希函数
        return Math.abs((element.hashCode() + seed) % size);
    }
}
  1. 应用场景:
  • 网页爬虫URL去重
  • 垃圾邮件过滤
  • 缓存穿透预防
  • 大数据集合的去重

布隆过滤器通过牺牲一定的准确性来换取空间效率,是一个在大数据场景下非常实用的数据结构。

项目大体框架

在这里插入图片描述

代码

  • BloomFilterConfig
package cn.yam.bloomfilter.config;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BloomFilterConfig {

    @Bean
    public BloomFilter<Long> bloomFilter() {
        // 预计要插入多少数据
        long expectedInsertions = 1000000;
        // 期望的误判率
        double fpp = 0.01; //  误判率,值越小,布隆过滤器的准确率越高,但会占用更多空间
        return BloomFilter.create(Funnels.longFunnel(), expectedInsertions, fpp);
    }
}
  • UserController
package cn.yam.bloomfilter.controller;


import cn.yam.bloomfilter.entity.User;
import cn.yam.bloomfilter.exception.InvalidParameterException;
import cn.yam.bloomfilter.exception.ResourceNotFoundException;
import cn.yam.bloomfilter.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.getUserById(id);
        if (user == null) {
            throw new InvalidParameterException("用户ID: " + id);
        }
        return ResponseEntity.ok(user);
    }
}
  • User
package cn.yam.bloomfilter.entity;

import lombok.Data;
import java.util.Date;

@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
    private Date createTime;
    private Date updateTime;
}
  • GlobalExceptionHandler
package cn.yam.bloomfilter.exception;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public Object handleAllExceptions(Exception ex) {
        Map<String, Object> response = new HashMap<>();
        response.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.put("message", ex.getMessage());
        return response;
    }

    @ExceptionHandler(InvalidParameterException.class)
    public ResponseEntity<Map<String, Object>> handleInvalidParameterException(InvalidParameterException ex) {
        Map<String, Object> response = new HashMap<>();
        response.put("status", HttpStatus.BAD_REQUEST.value());
        response.put("message", "参数错误: " + ex.getMessage());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

}
  • InvalidParameterException
package cn.yam.bloomfilter.exception;

public class InvalidParameterException extends RuntimeException {
    public InvalidParameterException(String message) {
        super(message);
    }
}
  • UserMapper
package cn.yam.bloomfilter.mapper;

import cn.yam.bloomfilter.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface UserMapper {
    User selectById(Long id);
    List<Long> selectAllIds();
}
  • UserMapper.xml
package cn.yam.bloomfilter.service.Impl;

import cn.yam.bloomfilter.entity.User;
import cn.yam.bloomfilter.mapper.UserMapper;
import cn.yam.bloomfilter.service.UserService;
import com.alibaba.fastjson.JSON;
import com.google.common.hash.BloomFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private BloomFilter<Long> bloomFilter;

    @PostConstruct//    @PostConstruct注解的方法将会在依赖注入完成后被自动调用
    public void init() {
        // 初始化布隆过滤器,将所有用户ID添加到过滤器中
        userMapper.selectAllIds().forEach(id -> bloomFilter.put(id));
        log.info("布隆过滤器初始化完成");
    }

    @Override
    public User getUserById(Long id) {
        // 1. 布隆过滤器判断
        if (!bloomFilter.mightContain(id)) {
            log.info("布隆过滤器中不存在该ID:{}", id);
            return null;
        }

        // 2. 查询缓存
        String key = "user:" + id;
        String userJson = redisTemplate.opsForValue().get(key);
        if (userJson != null) {
            log.info("从缓存中获取到数据,id:{}", id);
            return JSON.parseObject(userJson, User.class);
        }

        // 3. 查询数据库
        User user = userMapper.selectById(id);
        if (user != null) {
            // 放入缓存
            redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
            log.info("从数据库中获取到数据,并放入缓存,id:{}", id);
        }

        return user;
    }
}
  • UserService
package cn.yam.bloomfilter.service;

import cn.yam.bloomfilter.entity.User;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

public interface UserService {
    User getUserById(Long id);
}
  • UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.yam.bloomfilter.mapper.UserMapper">
    <select id="selectById" resultType="cn.yam.bloomfilter.entity.User">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <select id="selectAllIds" resultType="java.lang.Long">
        SELECT id
        FROM user
    </select>
</mapper>
  • application.yml
spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false
  application:
    name: BloomFilter
  datasource:
    url: jdbc:mysql://xxx:xxx/bloom_filter_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: xx
    password: xxx
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: localhost
    port: 6379
    database: 10
    password: 1234

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.yam.bloomfilter.entity
  configuration:
    map-underscore-to-camel-case: true
  • sql 建表语句
CREATE DATABASE bloom_filter_demo;
USE bloom_filter_demo;

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `age` int(11) NOT NULL,
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入一些测试数据
INSERT INTO `user` (`name`, `age`) VALUES
('user1', 25),
('user2', 30),
('user3', 35),
('user4', 40),
('user5', 45);

测试

  • 在应用启动时,会从数据库中加载所有用户 ID 到布隆过滤器
    @PostConstruct//    @PostConstruct注解的方法将会在依赖注入完成后被自动调用
    public void init() {
        // 初始化布隆过滤器,将所有用户ID添加到过滤器中
        userMapper.selectAllIds().forEach(id -> bloomFilter.put(id));
        log.info("布隆过滤器初始化完成");
    }

  • 实现了完整的缓存策略:布隆过滤器 -> Redis缓存 -> MySQL数据库
 @Override
    public User getUserById(Long id) {
        // 1. 布隆过滤器判断
        if (!bloomFilter.mightContain(id)) {
            log.info("布隆过滤器中不存在该ID:{}", id);
            return null;
        }

        // 2. 查询缓存
        String key = "user:" + id;
        String userJson = redisTemplate.opsForValue().get(key);
        if (userJson != null) {
            log.info("从缓存中获取到数据,id:{}", id);
            return JSON.parseObject(userJson, User.class);
        }

        // 3. 查询数据库
        User user = userMapper.selectById(id);
        if (user != null) {
            // 放入缓存
            redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
            log.info("从数据库中获取到数据,并放入缓存,id:{}", id);
        }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值