Redis:bitmap+布隆过滤器

本文详细介绍了位图的数据结构、命令及其在二值统计场景的应用,以及布隆过滤器的工作原理、如何使用位图实现并解决假阳性和删除元素的问题,同时提供了SpringBoot项目中布隆过滤器的代码示例。

本文将讲解bitmap的概念、命令、应用场景布隆过滤器的概念和使用bitmap实现布隆过滤器

bitmap

bitmap概念

bitmap,一种用于处理位操作的特殊数据结构,是一个由二进制位组成的字符串(即二进制数组,每一位都只能是0或1),常用于二值统计场景。

bitmap命令

  • SETBIT key offset value:将指定偏移量处的位设置为指定的值(0或1)。
  • GETBIT key offset:获取指定偏移量处的位的值。
  • BITCOUNT key [start end]:统计指定范围内位为1的数量。
  • BITOP operation destkey key [key ...]:对多个Bitmap进行位操作,将结果存储在目标键中,支持的位操作有AND、OR、XOR和NOT。
  • BITPOS key bit [start [end]]:查找指定位值(0或1)在Bitmap中的位置范围。

bitmap应用场景

任何二值统计场景都可以使用bitmap,如:

  • 签到、打卡应用。
  • 用户在线状态统计。

布隆过滤器

布隆过滤器是一种用于判断某个元素是否属于某个集合的特殊数据结构,其具有判断为无则不存在,判断为有则不一定存在的特性。

思想

当有变量被加入集合时,通过N个映射函数将这个变量映射成位图中的N个点, 把它们置为 1(假定有两个变量都通过 3 个映射函数)。

查询某个变量的时候我们只要看看这些点是不是都是 1, 就可以大概率知道集合中有没有它了:

  • 如果这些点,有任何一个为零则被查询变量一定不在。
  • 如果都是 1,则被查询变量很可能存在(因为存在哈希碰撞的情况)。

应用场景

布隆过滤器常用于预防缓存穿透,其存储已存在数据的key,当有新的请求时,先到布隆过滤器中查询是否存在:

  • 如果布隆过滤器中不存在该条数据则直接返回。
  • 如果布隆过滤器中已存在,才去查询缓存redis,如果redis里没查询到则再查询Mysql数据库。

另外也可以根据布隆过滤器数据结构的特点,用于进行黑名单、白名单校验等场景。

缺陷

布隆过滤器有两方面的缺陷,都是由哈希碰撞导致的:

  1. 如果判断为有,可能为无。
  2. 无法删除元素,否则会导致存在的其他元素被删除而判断为无。

代码实现

首先声明依赖:

<dependencies>
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.31</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.12.5</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.2</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.5.3.2</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.0.33</version>
    </dependency>
</dependencies>

本次将使用mybatis-plus-generator反向生成service,以便快速开始bloomfilter内容的展示。

在预估bitmap大小时,可以使用公式m = (-n * ln(p)) / (ln(2)^2),m为位图的大小,n是要存储的对象数量,p是期望的假阳性率。

如1000000数据下,1%假阳性需要9,585,058bit,即≈2^23,需要对hash结果mod2^23来控制位图大小。

BloomUtils.class:

@Component
public class BloomUtils {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    IEmployeesService service;
    public static final String KEY_NAME = "WhitelistEmployees";

    public void init() {
        //白名单客户预加载到布隆过滤器
        List<Employees> employees = service.getBaseMapper().selectList(new QueryWrapper<>());
        employees.forEach(employees1 -> {
            try {
                addToWhitelist(String.valueOf(employees1.getEmployeeId()));
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        });
    }
    
    //根据m = (-n * ln(p)) / (ln(2)^2)公式(m为位图的大小,n是要存储的对象数量,p是期望的假阳性率)。
    // 1000000数据下,1%假阳性需要9,585,058bit,即≈2^23,需要对hash结果mod2^23来控制位图大小。
    public void addToWhitelist(String value) throws NoSuchAlgorithmException {
        long md5Hash = (long) (hashToPositiveInt(value, "MD5") % Math.pow(2, 23));
        long sha1Hash = (long) (hashToPositiveInt(value, "SHA-1") % Math.pow(2, 23));
        long sha256Hash = (long) (hashToPositiveInt(value, "SHA-256") % Math.pow(2, 23));
        stringRedisTemplate.opsForValue().setBit(KEY_NAME, md5Hash, true);
        stringRedisTemplate.opsForValue().setBit(KEY_NAME, sha1Hash, true);
        stringRedisTemplate.opsForValue().setBit(KEY_NAME, sha256Hash, true);
    }

    public boolean checkInWhitelist(String value) throws NoSuchAlgorithmException {
        long md5Hash = (long) (hashToPositiveInt(value, "MD5") % Math.pow(2, 23));
        long sha1Hash = (long) (hashToPositiveInt(value, "SHA-1") % Math.pow(2, 23));
        long sha256Hash = (long) (hashToPositiveInt(value, "SHA-256") % Math.pow(2, 23));
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().getBit(KEY_NAME, md5Hash)) &&
                Boolean.TRUE.equals(stringRedisTemplate.opsForValue().getBit(KEY_NAME, sha1Hash)) &&
                Boolean.TRUE.equals(stringRedisTemplate.opsForValue().getBit(KEY_NAME, sha256Hash));
    }

    public static int hashToPositiveInt(String input, String algorithm) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance(algorithm);
        byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
        // 使用BigInteger将字节数组表示的哈希值转换为正整数
        BigInteger bigIntegerHash = new BigInteger(1, hashBytes);
        // 获取正整数表示的哈希值
        return bigIntegerHash.intValue();
    }
}

然后即可在Controller中直接调用:

@RestController
@RequestMapping("/employees")
public class EmployeesController {
    @Autowired
    IEmployeesService service;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    BloomUtils bloomUtils;

    @GetMapping("/init")
    public void initRedisWhitelist() {
        bloomUtils.init();
    }

    @GetMapping("/check")
    public boolean checkWhitelist(@RequestParam String employeesID) throws NoSuchAlgorithmException {
        return bloomUtils.checkInWhitelist(employeesID);
    }

    @GetMapping("/find")
    public Employees findByEmployeeById(@RequestParam String employeesID) throws NoSuchAlgorithmException, JsonProcessingException {
        if (bloomUtils.checkInWhitelist(employeesID)) {//先过布隆过滤器
            String Semployees = stringRedisTemplate.opsForValue().get(employeesID);
            Employees employees = null;
            if (Semployees == null) {
                synchronized (this) {
                    Semployees = stringRedisTemplate.opsForValue().get(employeesID);// 双检查,检查缓存中是否有数据
                    if (Semployees == null) {
                        employees = service.getById(employeesID);// 从数据库获取数据
                        stringRedisTemplate.opsForValue().set(employeesID, new ObjectMapper().writeValueAsString(employees));// 将数据写入缓存
                    }
                }
                return employees;
            } else {
                return new ObjectMapper().readValue(Semployees, Employees.class);
            }
        } else {//布隆过滤器不通过则直接返回null
            return null;
        }
    }

    @PostMapping("/add")
    public void addEmployees(@RequestBody Employees employees) throws JsonProcessingException, NoSuchAlgorithmException {
        service.save(employees);
        Employees employees1 = service.getById(employees.getEmployeeId());
        stringRedisTemplate.opsForValue().set(String.valueOf(employees1.getEmployeeId()),new ObjectMapper().writeValueAsString(employees1));
        bloomUtils.addToWhitelist(String.valueOf(employees1.getEmployeeId()));
    }
}

使用Redis布隆过滤器实现接口幂等的方法,可按如下步骤操作: ### 原理 布隆过滤器是一种空间有效的数据结构,用于高效地检查一个元素是否属于一个集合。接口幂等是指对同一操作的多次请求,系统的效果执行一次的效果相同。使用Redis布隆过滤器实现接口幂等的核心思路是,在请求接口时,先通过布隆过滤器检查请求标识是否已存在,若存在则直接返回之前的结果,若不存在则执行接口逻辑,并将请求标识存入布隆过滤器Redis中。 ### 实现步骤 #### 1. 初始化布隆过滤器 使用RedisBitMap来实现布隆过滤器,可使用多个哈希函数生成多个索引,以降低哈希冲突的概率。以下是示例代码: ```java import org.springframework.data.redis.core.RedisTemplate; public class BloomFilter { private RedisTemplate<String, Object> redisTemplate; private String redisKey; private int hashCount; public BloomFilter(RedisTemplate<String, Object> redisTemplate, String redisKey, int hashCount) { this.redisTemplate = redisTemplate; this.redisKey = redisKey; this.hashCount = hashCount; } // 计算多个哈希函数的索引 private long[] getIndices(String key) { long[] indices = new long[hashCount]; int abs = Math.abs(key.hashCode()); for (int i = 0; i < hashCount; i++) { indices[i] = (long) ((abs + i) % Math.pow(2, 32)); } return indices; } // 设置布隆过滤器的某个位置值为true public void add(String key) { long[] indices = getIndices(key); for (long index : indices) { redisTemplate.opsForValue().setBit(redisKey, index, true); } } // 查询某个位置的值 public boolean mightContain(String key) { long[] indices = getIndices(key); for (long index : indices) { if (!redisTemplate.opsForValue().getBit(redisKey, index)) { return false; } } return true; } } ``` #### 2. 实现接口幂等逻辑 在接口处理方法中,使用布隆过滤器Redis来实现幂等。示例代码如下: ```java import org.springframework.data.redis.core.RedisTemplate; import java.util.concurrent.TimeUnit; public class IdempotentService { private RedisTemplate<String, Object> redisTemplate; private BloomFilter bloomFilter; public IdempotentService(RedisTemplate<String, Object> redisTemplate, BloomFilter bloomFilter) { this.redisTemplate = redisTemplate; this.bloomFilter = bloomFilter; } public Object processRequest(String requestId) { // 先通过布隆过滤器检查请求标识是否已存在 if (bloomFilter.mightContain(requestId)) { // 若存在,从Redis中获取之前的结果 Object result = redisTemplate.opsForValue().get(requestId); if (result != null) { return result; } } // 若不存在,执行接口逻辑 Object result = executeInterfaceLogic(); // 将请求标识存入布隆过滤器 bloomFilter.add(requestId); // 将结果存入Redis,并设置过期时间 redisTemplate.opsForValue().set(requestId, result, 60, TimeUnit.MINUTES); return result; } private Object executeInterfaceLogic() { // 这里是具体的接口业务逻辑 return "接口执行结果"; } } ``` ### 3. 使用示例 ```java import org.springframework.data.redis.core.RedisTemplate; public class Main { public static void main(String[] args) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); // 初始化布隆过滤器 BloomFilter bloomFilter = new BloomFilter(redisTemplate, "bloom_filter_key", 3); // 初始化幂等服务 IdempotentService idempotentService = new IdempotentService(redisTemplate, bloomFilter); String requestId = "unique_request_id"; Object result = idempotentService.processRequest(requestId); System.out.println(result); } } ``` ### 注意事项 - 布隆过滤器存在一定的误判率,即可能会误判某个元素存在于集合中,但不会误判元素不存在。因此,在从Redis中获取结果时,仍需检查结果是否为空。 - 为了避免Redis中的数据过多,可设置合理的过期时间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值