缓存击穿
缓存穿透:指查询一个不存在的数据,缓存中没有相应的记录,每次请求都会去数据库查询,造成数据库负担加重。
解决方案一
布隆过滤器
布隆过滤器(Bloom Filter)是一个节省空间的概率型数据结构,用于判断一个元素是否在集合中。以下是它的主要原理:
- 基本组成:
- 一个长度为 m 的位数组(初始全部为0)
- k 个不同的哈希函数
- 工作原理:
插入元素:
1. 当插入一个元素时,使用 k 个哈希函数计算出 k 个哈希值
2. 将位数组中对应的 k 个位置都设为 1
查询元素:
1. 同样使用 k 个哈希函数计算出 k 个哈希值
2. 检查位数组中对应的 k 个位置
3. 如果有任何一个位置为 0,则该元素一定不在集合中
4. 如果所有位置都为 1,则该元素可能在集合中(存在误判)
- 特点:
- 空间效率高
- 不支持删除操作
- 有一定的误判率(False Positive)
- 不会漏判(False Negative)
- 示例代码:
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);
}
}
- 应用场景:
- 网页爬虫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);
}