Redis中怎么实现布隆过滤器

127 阅读6分钟

Redis怎么使用布隆过滤器

什么是布隆过滤器

布隆过滤器 (Bloom Filter)是由 Burton Howard Bloom 于 1970 年提出,它是一种 space efficient 的概率型数据结构,用于判断一个元素是否在集合中。

当布隆过滤器说,某个数据存在时,这个数据可能不存在;当布隆过滤器说,某个数据不存在时,那么这个数据一定不存在。

哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的 1/8 或 1/4 的空间复杂度就能完成同样的问题。

布隆过滤器可以插入元素,但不可以删除已有元素。

其中的元素越多,false positive rate(误报率)越大,但是 false negative (漏报)是不可能的。

布隆过滤器原理

  • bitmap:是一个二进制数组
  • 布隆过滤器的作用:布隆过滤器可以检索一个元素是否存在一个集合中

布隆过滤器会通过 3 个操作完成标记:

  • 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;
  • 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
  • 第三步,将每个哈希值在位图数组的对应位置的值设置为 1;

举个例子,假设有一个位图数组长度为 8,哈希函数 3 个的布隆过滤器。

在数据库写入数据 x 后,把数据 x 标记在布隆过滤器时,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果为 1、4、6,然后把位图数组的第 1、4、6 位置的值设置为 1。当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中

布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。

所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据

使用场景

  • 解决缓存穿透
  • 可以用来判断一个值是否存在(有一定概率误判)

布隆过滤器

布隆过滤器的安装

一、版本要求

●推荐版本6.x,最低4.x版本,可以通过如下命令查看版本:
查看redis的版本

redis-server --version

●插件安装,网上大部分推荐v1.1.1,文章写的时候v2.2.6已经是release版本了,用户自己选择,地址全在下面(2.2.6官网介绍说是1.0版本的维护版本,如果不想使用新的功能,无需升级!)

二、安装&编译

下载插件压缩包

wget https://github.com/RedisLabsModules/rebloom/archive/v2.2.6.tar.gz

执行上述命令后,你应该会看到类似以下的输出,表示文件正在下载:

--2024-07-03 08:00:00--  https://github.com/RedisLabsModules/rebloom/archive/v2.2.6.tar.gz
Resolving github.com (github.com)... 140.82.121.4
Connecting to github.com (github.com)|140.82.121.4|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1234567 (1.2M) [application/octet-stream]
Saving to: ‘v2.2.6.tar.gz’

v2.2.6.tar.gz                     100%[================================================>]   1.18M  --.-KB/s    in 0.05s

2024-07-03 08:00:01 (25.3 MB/s) - ‘v2.2.6.tar.gz’ saved [1234567/1234567]

解压缩文件

tar -xzvf v2.2.6.tar.gz

编译插件

cd RedisBloom-2.2.6/
make

image.png

三、Redis集成

方式一

通过命令方式挂载

redis-server  --loadmodule /usr/local/RedisBloom-2.2.6/redisbloom.so 

注意:要写自己的redisbloom.so的地址

方式二

在配置文件里添加
image.png

启动redis

四、测试

image.png

使用布隆过滤器

“纸上谈来终觉浅,绝知此事要躬行”,接下来让我们实战下如何通过布隆过滤器避免订单信息查询的缓存穿透问题。

一、引入依赖

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.16.7</version>
</dependency>
 <!--mybatis-plus-->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.4.2</version>
</dependency>
<!--mysql-->
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>

该redisson依赖已经包含的redis的依赖,可以不用引入redis的依赖。

二、创建订单表

CREATE TABLE `tb_order` (
  `id` bigint(20) NOT NULL COMMENT '订单Id',
  `order_desc` varchar(50) NOT NULL COMMENT '订单描述',
  `user_id` bigint(20) NOT NULL COMMENT '用户Id',
  `product_id` bigint(20) NOT NULL COMMENT '商品Id',
  `product_num` int(11) NOT NULL COMMENT '商品数量',
  `total_account` decimal(10,2) NOT NULL COMMENT '订单金额',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `ik_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

三、配置文件

spring:
  application:
    name: redis-demo
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ******
    url: jdbc:mysql://localhost:3306/study?useSSL=false&serverTimezone=UTC
  redis:
    port: 6379
    host: 192.168.38.129
    password: 123456
    database: 7

# mybatis-plus配置
mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml

四、代码实现

/**
 * 配置布隆过滤器
 */
@Configuration
public class BloomFilterConfig {
    @Autowired
    private RedissonClient redissonClient;
    /**
     * 创建订单号布隆过滤器
     * @return
     */
    @Bean
    public RBloomFilter<Long> orderBloomFilter() {
        //过滤器名称
        String filterName = "orderBloomFilter";
        // 预期插入数量
        long expectedInsertions = 10000L;
        // 错误比率
        double falseProbability = 0.01;
        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(filterName);
        bloomFilter.tryInit(expectedInsertions, falseProbability);
        return bloomFilter;
    }
}

创建订单

redisson中的BloomFilter有2个核心方法:

  • bloomFilter.add(orderId) 向布隆过滤器中添加id
  • bloomFilter.contains(orderId) 判断id是否存在
import com.alibaba.fastjson.JSON;
import com.ycl.redisspringbootdemo.mapper.OrderMapper;
import com.ycl.redisspringbootdemo.pojo.Order;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;


/**
 * @Author ycl
 * @Date 2024-07-09 16:13
 */
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Autowired
    private RBloomFilter<Long> bloomFilter;

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 创建订单
     * @param order
     */
    @PostMapping("/create")
    public Long createOrder(@RequestBody Order order ) {
        // 生成订单(这里就用时间戳代表订单号了)
        Long id=System.currentTimeMillis();

        // 将订单号添加到布隆过滤器中
        boolean res = bloomFilter.add(id);
        if(res){
            order.setId(id);
            // 存入数据库中
            orderMapper.insert(order);
            // 将订单信息存入redis中
            String jsonString = JSON.toJSONString(order);
            stringRedisTemplate.opsForValue().set(Long.toString(id),jsonString);
            return id;
        }
        return 0L;
    }

    /**
     * 查询订单
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Order query(@PathVariable Long id) {
        // 先从布隆过滤器中判断时候有此订单
        boolean res = bloomFilter.contains(id);
        if(!res){
            return null;
        }
        // 从缓存中判断是否有该订单
        String s = stringRedisTemplate.opsForValue().get(Long.toString(id));
        if(!StringUtils.hasText(s)){
            return orderMapper.selectById(id);
        }
        Order order = JSON.parseObject(s, Order.class);
        log.info("订单:{}",order);
        return order;
    }
}

json数据

{
  "orderDesc": "This is a sample order description",
  "userId": 123,
  "productId": 456,
  "productNum": 2,
  "totalAccount": 99.99,
  "createTime": "2024-07-09T15:10:16.927"
}

测试 查询一个不存在的订单号。

GET http://localhost:8080/order/123

通过控制台输出可以看出,并没有输出日志,也就是说通过布隆过滤器判断这个订单号是不存在的。

image.png