1. 业务需求
业务中由于是分布式前后端分离项目,使用jwt的方式;
为了提高用户体验,要求通过快要失效的token去通过接口调用获取新token;
调用要求:一个用于刷新的refreshToken只能调用一次即失效,因此需要将每次来的token缓存起来,每次新来的token去缓存中查看是否存在,如果不存在(即首次调用)则返回新token,如果存在则不再返回;
2. 解决策略
- 布隆过滤器
布隆过滤器(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);
}
这种方法基本上能完成需求,但一旦服务重启,内存中的数据就全部丢失,未能持久化
- 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设置过期时间的
- 总结
bloomfilter+redis的组合能兼顾持久化和占用内存小的,同时给bitmap加上过期时间,避免了bitmap一直存在导致的错误率不断提升的问题;
此种解决方案,仍有极小概率导致误判,但事实上,即使当前场景发生了误判也没太大影响!
由于个人能力有限,如有错误还请留言指正!