布隆过滤器bloomfilter结合redis实现持久化且支持周期性重置

探讨如何在分布式系统中使用JWT并结合布隆过滤器和Redis优化刷新令牌策略,减少内存消耗,确保安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 业务需求

业务中由于是分布式前后端分离项目,使用jwt的方式;
为了提高用户体验,要求通过快要失效的token去通过接口调用获取新token;
调用要求:一个用于刷新的refreshToken只能调用一次即失效,因此需要将每次来的token缓存起来,每次新来的token去缓存中查看是否存在,如果不存在(即首次调用)则返回新token,如果存在则不再返回;

2. 解决策略

  1. 布隆过滤器
    布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k
    在这里插入图片描述
    具体原理我在此由于篇幅原因就不解释了,只说一个它的特点;

它不能保证数据一定在里面,但它能保证一定不在里面

最简单的使用方法就是用guava中的bloomfilter;
依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>22.0</version>
</dependency>
private final BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1024*1024*32, 0.0000001d);
...
        boolean exists = bloomFilter .mightContain(refreshToken);
        if(!exists){
            bloomFilter .put(refreshToken);
        }

这种方法基本上能完成需求,但一旦服务重启,内存中的数据就全部丢失,未能持久化

  1. redis做缓存
    直接存入redis,并设置一个过期时间
String refreshToken = redisTemplate.opsForValue().get(spaceId);
if(StringUtils.isNotBlank(refreshToken)) return null;
...
...
...
//在将token放入缓存中
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.HOURS);

这里会将大量的token缓存起来,由于jwt的token每个字符串都算比较长的了,这会导致大量的内存被占用,考虑有没有改进之法;

3.bloomfilter+redis
将上述两种方式结合一起,可以利用redis中的bitmap来持久化bloomfilter中的位数组;

BloomFilterHelper

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;
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
    }

    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;
        }
        return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
    }

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


}

BloomfilterService

@Slf4j
@Service
public class BloomfilterService {

    @Value("${bloomfilter.expiration}")
    private long expiration;

    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 根据给定的布隆过滤器添加值
     */
    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);
        }
        //由于setBit不能直接设置过期时间,因此另外再设置
        //返回值expire为-1时 此键值没有设置过期日期
        //返回值expire为-2时 不存在此键
        long expire = redisTemplate.opsForValue().getOperations().getExpire(key);//此方法返回单位为秒过期时长
        if (expire == -1 ){
            redisTemplate.expire(key,expiration, TimeUnit.HOURS);
        }
    }

    /**
     * 根据给定的布隆过滤器判断值是否存在
     */
    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;
    }


}

Model


@Data
@NoArgsConstructor
@AllArgsConstructor
public class Model implements Serializable {

    private String key;

    private String value;

}

重点UserServiceImpl

@Slf4j
@Service
public class UserServiceImpl implements UserService {

    private final BloomFilterHelper<Model> modelBloomFilterHelper = new BloomFilterHelper<>((Funnel<Model>)
            (from, into) -> into.putString(from.getKey(), Charsets.UTF_8).putString(from.getValue(), Charsets.UTF_8),
            100000, 0.000001);


@Override
    public Map<String, String> createTokenByRefreshToken(String refreshToken) {
		...
		//查询是否存在
		boolean includeFlag = bloomfilterService.includeByBloomFilter(modelBloomFilterHelper, "modelFilter", model);
        if (includeFlag) return null;
		...
		//添加
		bloomfilterService.addByBloomFilter(modelBloomFilterHelper,"modelFilter",model);
	}

}            

经过测试,能够很好的实现我们的需求
但我理想中还是希望能实现重置,即对于此bitmap设置一个过期时间
继续
BloomfilterService 中的addByBloomFilter

    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);
        }
        //*********************************************************
        //由于setBit不能直接设置过期时间,因此另外再设置
        //返回值expire为-1时 此键值没有设置过期日期
        //返回值expire为-2时 不存在此键
        long expire = redisTemplate.opsForValue().getOperations().getExpire(key);//此方法返回单位为秒过期时长
        if (expire == -1 ){
            redisTemplate.expire(key,expiration, TimeUnit.HOURS);
        }
        //*********************************************************

    }

*分割线中的部分给整个key加入了周期性的设置过期时间;
由于redisTemplate.opsForValue().setBit()这个方法中不能直接带上过期时间,经api查看后,发现是支持单独给某个key设置过期时间的

  1. 总结
    bloomfilter+redis的组合能兼顾持久化和占用内存小的,同时给bitmap加上过期时间,避免了bitmap一直存在导致的错误率不断提升的问题;
    此种解决方案,仍有极小概率导致误判,但事实上,即使当前场景发生了误判也没太大影响!
    由于个人能力有限,如有错误还请留言指正!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值