布隆过滤器特点:误判只会发生在过滤器没有添加过的元素,对于添加过的元素不会发生误判
Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器
public class GuavaBloomfilterDemo {
public static final int _1W = 10000;
//布隆过滤器里预计要插入多少数据
public static int size = 100 * _1W;
//误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
public static double fpp = 0.01; //误判率特别小时,程序执行效率急剧下降
/**
* helloworld入门
*/
public void bloomFilter() {
// 创建布隆过滤器对象
BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));//false
System.out.println(filter.mightContain(2));//false
// 将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));//true
System.out.println(filter.mightContain(2));//true
}
/**
* 误判率演示+源码分析
*/
public void bloomFilter2() {
// 构建布隆过滤器
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
//1 先往布隆过滤器里面插入100万的样本数据
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
/* List listSample = new ArrayList<>(size);
//2 这100万的样本数据,是否都在布隆过滤器里面存在?
for (int i = 0; i < size; i++){
if (bloomFilter.mightContain(i)) {
listSample.add(i);
continue;
}
}
System.out.println("存在的数量:" + listSample.size());//存在的数量:1000000 */
//3 故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里,误判率演示
List<Integer> list = new ArrayList<>(10 * _1W);
for (int i = size + 1; i < size + 100000; i++) {
if (bloomFilter.mightContain(i)) {
System.out.println(i + "\t" + "被误判了.");
list.add(i);
}
}
System.out.println("误判的数量:" + list.size()); //误判的数量:947
}
public static void main(String[] args) {
new GuavaBloomfilterDemo().bloomFilter2();
}
}
Guava缺点说明:只能单机使用
案例:白名单过滤器
public class RedissonBloomFilterDemo {
public static final int _1W = 10000;
//布隆过滤器里预计要插入多少数据
public static int size = 100 * _1W;
//误判率,它越小误判的个数也就越少
public static double fpp = 0.03;
static RedissonClient redissonClient = null;//jedis
static RBloomFilter rBloomFilter = null;//redis版内置的布隆过滤器
@Resource
RedisTemplate redisTemplate;
static {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);
//构造redisson
redissonClient = Redisson.create(config);
//通过redisson构造rBloomFilter
rBloomFilter = redissonClient.getBloomFilter("phoneListBloomFilter", new StringCodec());
rBloomFilter.tryInit(size, fpp);
// 1测试 布隆过滤器有+redis有
//rBloomFilter.add("10086");
//redissonClient.getBucket("10086",new StringCodec()).set("chinamobile10086");
// 2测试 布隆过滤器有+redis无
//rBloomFilter.add("10087");
//3 测试 ,布隆过滤器无+redis无
}
private static String getPhoneListById(String IDNumber) {
String result = null;
if (IDNumber == null) {
return null;
}
//1 先去布隆过滤器里面查询
if (rBloomFilter.contains(IDNumber)) {
//2 布隆过滤器里有,再去redis里面查询
RBucket<String> rBucket = redissonClient.getBucket(IDNumber, new StringCodec());
result = rBucket.get();
if (result != null) {
return "i come from redis: " + result;
} else {
result = getPhoneListByMySQL(IDNumber);
if (result == null) {
return null;
}
// 重新将数据更新回redis
redissonClient.getBucket(IDNumber, new StringCodec()).set(result);
}
return "i come from mysql: " + result;
}
return result;
}
private static String getPhoneListByMySQL(String IDNumber) {
return "chinamobile" + IDNumber;
}
public static void main(String[] args) {
//String phoneListById = getPhoneListById("10086");
//String phoneListById = getPhoneListById("10087"); //请测试执行2次
String phoneListById = getPhoneListById("10088");
System.out.println("------查询出来的结果: " + phoneListById);
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
redissonClient.shutdown();
}
}
docker run -p 6379:6379 --name=redis6379bloom -d redislabs/rebloom
docker exec -it redis6379bloom /bin/bash
redis-cli
大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去
简单说就是热点key突然失效了,暴打mysql
会造成某一时刻数据库请求量过大,压力剧增。
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
@Service
@Slf4j
public class JHSABTaskService {
@Autowired
private RedisTemplate redisTemplate;
//@PostConstruct ,平时注释,用时打开
public void initJHSAB() {
log.info("启动AB定时器计划任务淘宝聚划算功能模拟.........." + DateUtil.now());
new Thread(() -> {
//模拟定时器,定时把数据库的特价商品,刷新到redis中
while (true) {
//模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
List<Product> list = this.products();
//先更新B缓存
this.redisTemplate.delete(Constants.JHS_KEY_B);
this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_B, list);
this.redisTemplate.expire(Constants.JHS_KEY_B, 20L, TimeUnit.DAYS);
//再更新A缓存
this.redisTemplate.delete(Constants.JHS_KEY_A);
this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_A, list);
this.redisTemplate.expire(Constants.JHS_KEY_A, 15L, TimeUnit.DAYS);
//间隔一分钟 执行一遍
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("runJhs定时刷新..............");
}
}, "t1").start();
}
/**
* 模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
*/
public List<Product> products() {
List<Product> list = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
Random rand = new Random();
int id = rand.nextInt(10000);
Product obj = new Product((long) id, "product" + i, i, "detail");
list.add(obj);
}
return list;
}
}
@RestController
@Slf4j
@Api(description = "聚划算商品列表接口AB")
public class JHSABProductController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value = "/pruduct/findab", method = RequestMethod.GET)
@ApiOperation("按照分页和每页显示容量,点击查看AB")
public List<Product> findAB(int page, int size) {
List<Product> list = null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
//采用redis list数据结构的lrange命令实现分页查询
list = this.redisTemplate.opsForList().range(Constants.JHS_KEY_A, start, end);
if (CollectionUtils.isEmpty(list)) {
log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
//用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
this.redisTemplate.opsForList().range(Constants.JHS_KEY_B, start, end);
}
log.info("查询结果:{}", list);
} catch (Exception ex) {
//这里的异常,一般是redis瘫痪 ,或 redis网络timeout
log.error("exception:", ex);
//TODO 走DB查询
}
return list;
}
}