Redis简介
1.什么是Redis?
Redis 是完全开源的,遵守 BSD 协议,是一个C语言开发的一个高性能的 key-value 数据库,可以用来做数据库、缓存、消息中间件等场景,是一种NoSQL(not-only sql,非关系型数据库)的数据库。
2.Redis特点
- 数据是存储在内存中,读写速度非常快,可支持并发10W QPS。
- 单线程单进程,是线程安全的,采用IO 多路复用制。
- 支持简单的事务。
- 支持五种数据类型。
- 支持数据持久化到磁盘。
- 可以作为消息中间件使用,支持消息发布及订阅。
3.Redis的五种数据类型
1)string(字符串)
string类型是Redis中最基本的数据存储类型,它是一个由字节组成的序列,在Redis中是二进制安全的。这意味着该类型可以接受任何格式数据,如JPEG图像数据和Json对象说明信息。它是标准的key-value,通常用于存储字符串、整数和浮点。Value可容纳高达512MB的数据。
常见操作string的Redis 命令:
SET key value
– 设置指定键的值。GET key
– 检索指定键的值。DEL key
– 删除给定键的值。
应用场景:用于计算站点访问量、当前在线人数等。
2)hash(哈希)
hash 是一个键值(key=>value)对集合。hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。Redis的Hash结构可以使你像在数据库中Update一个属性一样只修改某一项属性值。
常见操作hash 的Redis 命令:
HSET
– 将值映射到哈希中的键。HGET
– 检索与哈希中的键关联的各个值。HGETALL
– 显示整个哈希内容。HDEL
– 从哈希中删除现有的键值对。
应用场景:存储部分更改数据,如用户信息、会话共享。
3) list(列表)
list是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。Redis的列表允许用户从序列的两端推入或者弹出元素,列表由多个字符串值组成的有序可重复的序列,是链表结构,所以向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。
常见操作list的Redis 命令:
LPUSH
– 将值推送到列表的左端。RPUSH
– 将值推送到列表的尾端。LRANGE
– 检索一系列项目。LPOP/RPOP
– 用于显示和删除两端的项目。LINDEX
– 从列表中的特定位置获取值。
应用场景:最新消息排行榜;消息队列,以完成多程序之间的消息交换。
4) set(集合)
Set 是 string 类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。所谓集合就是一堆不重复值的组合,并且是没有顺序的
常见操作Set 的Redis 命令:
SADD
– 向集合中添加一个或多个项目。
– 找出一个项目是否是集合的一部分。SISMEMBER
SMEMBERS
– 从集合中检索所有项目。SREM
– 从集合中删除现有项。
应用场景:例如在微博应用中,用户的关注用户集合、粉丝集合。
5)sorted set (有序集合)
sorted set也叫Redis zset ,和set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。
常见操作sorted set的Redis 命令:
ZADD
– 将具有分数的成员添加到排序集。ZRANGE
– 根据项目在排序顺序中的位置检索项目。withscores
选项生成实际分数值。ZRANGEBYSCORE
– 根据定义的分数范围从排序集中提取项目。withscores
选项生成实际分数值。ZREM
–从已排序的集中删除项目。
使用场景:带有权重的元素,比如一个游戏的用户得分排行榜;比较复杂的数据结构,一般用到的场景不算太多。
4.Redis持久化
Redis是一个基于内存的数据库,它的数据是存放在内存中,内存有个问题就是关闭服务或者断电会丢失。Redis的数据也支持写到硬盘中,这个过程就叫做持久化。
Redis的2种持久化方式:
- RDB(Redis DataBase):简而言之,就是在指定的时间间隔内,定时的将 redis 存储的数据生成Snapshot快照并存储到磁盘等介质上。(redis默认的备份方式)
- AOF(Append Of File):将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
1)RDB快照原理
Redis 使用操作系统的多进程 COW(Copy On Write)
机制来实现快照持久化。
fork(多进程)
Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。
RDB 快照的触发方式:
- 自动触发,需要在redis.conf 文件里面的配置,格式:
save 秒针 写操作次数
- 手动触发,执行save或者 bgsave命令。
save:save时只管保存,其他不管,全部阻塞,手动保存,不建议使用。
bgsave:redis会在后台异步进行快照操作,快照同时还可以响应客户端情况。
- 主从复制的时候触发。
RDB的优缺点:
优势:
- 适合大规模数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 基于二进制存储的,恢复速度快
劣势:
- Fork的时候,内存中的数据会被克隆一份,大致2倍的膨胀,需要考虑
- 虽然Redis在fork的时候使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down的话,就会丢失最后一次快照后所有修改
2)AOF原理
客户端的请求写命令会被append追加到AOF缓冲区内,AOF缓冲区会根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中,AOF文件大小超过重写策略或手动重写时,会对AOF文件进行重写(rewrite),压缩AOF文件容量redis服务器重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的。
AOF配置:在redis.conf 文件中对AOF进行配置开启
appendonly yes
持久化策略:
- always:总是保存,每执行一个命令保存一次。
- everysec:每一秒钟保存一次。
- no:不保存,操作系统来决定什么时候保存保存。
AOF优缺点:
优势:
- 备份机制更稳健,丢失数据概率更低
- 可读的日志文本,通过操作AOF文件,可以处理误操作
劣势:
- 比RDB占用更多的磁盘空间
- 恢复备份速度要慢
- 每次读写都同步的话,有一定的性能压力
RDB持久化 | AOF持久化 |
全量备份,一次保存整个数据库。 | 增量备份,一次只保存一个修改数据车的命令。 |
每次执行持久化操作的间隔时间较长。 | 保存的间隔默认为一秒钟(Everysec)。 |
数据保存为二进制格式,其还原速度快。 | 使用文本格式还原数据,所以数据还原速度一般。 |
执行SAVE命令时会阻塞服务器,但手动或者自动触发的BGSVE不会阻塞服务器。 | AOF持久化无论何时都不会阻塞服务器。 |
5.Redis的淘汰策略
8种淘汰策略:
- volatile-lru,针对设置了过期时间的key,使用lru算法进行淘汰。
- allkeys-lru,针对所有key使用lru算法进行淘汰。
- volatile-lfu,针对设置了过期时间的key,使用lfu算法进行淘汰。
- allkeys-lfu,针对所有key使用lfu算法进行淘汰。
- volatile-random,从所有设置了过期时间的key中使用随机淘汰的方式进行淘汰。
- allkeys-random,针对所有的key使用随机淘汰机制进行淘汰。
- volatile-ttl,删除生存时间最近的一个键。
- noeviction(默认),不删除键,值返回错误。
主要是4种算法,针对不同的key,形成的策略。
4种算法:
- lru 最近很少的使用的key(根据时间,最不常用的淘汰)
- lfu 最近很少的使用的key (根据计数器,用的次数最少的key淘汰)
- random 随机淘汰
- ttl 快要过期的先淘汰
key:
- volatile 有过期的时间的那些key
- allkeys 所有的key
6.Redis缓存雪崩、缓存穿透和缓存击穿
1)缓存雪崩
缓存雪崩 ,就是存储在缓存里面的大量数据 ,在同一个时刻全部过期,原本缓存组件抗住的大部分流量全部请求到了数据库。导致数据库压力增加造成数据库服务器崩溃的现象。
缓存雪崩的解决方案:
- 设置这个缓存的失效时间,不让大量的 key 在同一时间失效,即在设置这个缓存的时候,可以将 key 的失效时间分散开。
- 我们部署 redis 一般是集群部署的,可以把这些热点的 key 放到不同的节点上去,让这些热点的 key 均匀的分布在不同的 redis 节点上。
- 还有就是比较暴力的方法,不设置这个缓存失效的时间,让 key 永不失效。
2)缓存穿透
缓存穿透 ,表示是短时间内有大量的不存在的 key 请求到应用里面,而这些不存在
的 key 在缓存里面又找不到 ,从而全部穿透到了数据库 ,造成数据库压力。
缓存穿透的解决方案:
- 请求如果穿透 redis,直接到数据库,那么数据库无论查出什么结果,都写回到 redis 缓存里面去,这样下次用同一个参数发来请求的时候,就直接被 redis 缓存拦截掉了,就不回打到数据库了。
- 对请求的参数做合法性校验。
- 比较直接、简单粗暴的方法,把这个 IP 拉黑。
- 最后是使用布隆过滤器,这是一个非常好的方式。
3)缓存击穿
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,造成数据库压力。
缓存击穿的解决方案:
- 让这个热点 key 不过期,即不设置失效时间(不推荐)。
- 使用分布式锁,如果是单体应用的话使用互斥锁。
7.Redis集群
Redis集群方式共有三种:主从模式,哨兵模式,cluster(集群)模式。
主从模式:是三种集群方式里最简单的。它主要是基于Redis的主从复制特性的。通常我们会设置一个主节点,N个从节点;默认情况下,主节点负责处理使用者的IO操作,而从节点则会对主节点的数据进行备份,并且也会对外提供读操作的处理。
主要的特点:
- 主从模式下,当某一节点损坏时,因为其会将数据备份到其它Redis实例上,这样做在很大程度上可以恢复丢失的数据。
- 主从模式下,主节点和从节点是读写分离的。使用者不仅可以从主节点上读取数据,还可以很方便的从从节点上读取到数据,这在一定程度上缓解了主机的压力。
- 从节点也是能够支持写入数据的,只不过从从节点写入的数据不会同步到主节点以及其它的从节点下。
Redis在主从模式下,必须保证主节点不会宕机——一旦主节点宕机,其它节点不会竞争称为主节点,此时,Redis将丧失写的能力。这点在生产环境中,是致命的。
哨兵模式:是基于主从模式做的一定变化,它能够为Redis提供了高可用性。在实际生产中,服务器难免不会遇到一些突发状况:服务器宕机,停电,硬件损坏等。这些情况一旦发生,其后果往往是不可估量的。而哨兵模式在一定程度上能够帮我们规避掉这些意外导致的灾难性后果。其实,哨兵模式的核心还是主从复制。只不过相对于主从模式在主节点宕机导致不可写的情况下,多了一个竞选机制——从所有的从节点竞选出新的主节点。竞选机制的实现,是依赖于在系统中启动一个sentinel进程。
sentinel特点:
- 监控:它会监听主服务器和从服务器之间是否在正常工作。
- 通知:它能够通过API告诉系统管理员或者程序,集群中某个实例出了问题。
- 故障转移:它在主节点出了问题的情况下,会在所有的从节点中竞选出一个节点,并将其作为新的主节点。
- 提供主服务器地址:它还能够向使用者提供当前主节点的地址。这在故障转移后,使用者不用做任何修改就可以知道当前主节点地址。
当竞选出新的主节点后,被选为新的主节点的从节点的配置信息会被sentinel改写为旧的主节点的配置信息。完成改写后,再将新主节点的配置广播给所有的从节点。
Redis集群(cluster) Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。
Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误。Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。
Redis 集群的优势:
- 自动分割数据到不同的节点上。
- 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。
- Redis 集群的数据分片 Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念.
Redis的使用
这里主要讲的是SpringBoot+Redis,对Redis的基本操作,以及Redis的应用。
1.在Java中操作Redis(5种基础数据类型)
添加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
编写application.properties:
spring.redis.host=127.0.0.1 spring.redis.port=6379 #指定数据库 spring.redis.database=10
编写配置类:
@Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ log.info("开始创建redis模板对象..."); RedisTemplate redisTemplate = new RedisTemplate(); //设置redis的连接工厂对象 redisTemplate.setConnectionFactory(redisConnectionFactory); //设置redis key的序列化器 redisTemplate.setKeySerializer(new StringRedisSerializer()); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 设置value的序列化规则和 key的序列化规则 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
测试代码:
@RunWith(SpringRunner.class) @SpringBootTest(classes = BoyanRedisdemoApplication.class) public class SpringDataRedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedisTemplate(){ System.out.println("redisTemplate:"+redisTemplate); ValueOperations valueOperations = redisTemplate.opsForValue(); ListOperations listOperations = redisTemplate.opsForList(); SetOperations setOperations = redisTemplate.opsForSet(); ZSetOperations zSetOperations = redisTemplate.opsForZSet(); HashOperations hashOperations = redisTemplate.opsForHash(); } @Test public void testString(){ redisTemplate.opsForValue().set("name","aa"); String name=(String) redisTemplate.opsForValue().get("name"); System.out.println("name:"+name); redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES); redisTemplate.opsForValue().setIfAbsent("lock","1"); redisTemplate.opsForValue().setIfAbsent("lock","2"); } @Test public void testHash(){ HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.put("100","name","tom"); hashOperations.put("100","age","20"); Student student=new Student(); student.setSno(110); student.setSname("小a"); student.setSage(20); hashOperations.put("stu",String.valueOf(student.getSno()),student); System.out.println(hashOperations.keys("stu")); System.out.println(hashOperations.values("stu")); } @Test public void testList(){ ListOperations listOperations = redisTemplate.opsForList(); listOperations.leftPushAll("mylist","a","b","c"); listOperations.leftPush("mylist","d"); List mylist = listOperations.range("mylist", 0, -1); System.out.println(mylist); listOperations.rightPop("mylist"); Long size = listOperations.size("mylist"); System.out.println(size); } @Test public void testSet(){ SetOperations setOperations = redisTemplate.opsForSet(); setOperations.add("set1","a","b","c","d"); setOperations.add("set2","a","b","x","y"); Set members = setOperations.members("set1"); System.out.println(members); Long size = setOperations.size("set1"); System.out.println(size); Set intersect = setOperations.intersect("set1", "set2"); System.out.println(intersect); Set union = setOperations.union("set1", "set2"); System.out.println(union); setOperations.remove("set1","a","b"); } @Test public void testZset() { ZSetOperations zSetOperations = redisTemplate.opsForZSet(); zSetOperations.add("zset1", "a", 10); zSetOperations.add("zset1", "b", 12); zSetOperations.add("zset1", "c", 9); Set zset1 = zSetOperations.range("zset1", 0, -1); System.out.println(zset1); zSetOperations.incrementScore("zset1", "c", 10); zSetOperations.remove("zset1", "a", "b"); } @Test public void testCommon(){ Set keys = redisTemplate.keys("*"); System.out.println(keys); Boolean name = redisTemplate.hasKey("name"); Boolean set1 = redisTemplate.hasKey("set1"); for (Object key : keys) { DataType type = redisTemplate.type(key); System.out.println(type.name()); } redisTemplate.delete("mylist"); } }
2.Redis实现接口幂等
幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。
redis实现幂等的原理图:
代码实现:
参考:https://mp.weixin.qq.com/s/1D-8TUtJq74mrDqlBgDgEw
Controlle代码:
@RestController @RequestMapping("business") public class BusinessController { @Resource private TokenService tokenService; @Resource private StudentService studentService; @PostMapping("/get/token") public ResultVo getToken(){ String token = tokenService.createToken(); if (StringUtils.isNotEmpty(token)) { ResultVo resultVo = new ResultVo(); resultVo.setCode("200"); resultVo.setMessage("success"); resultVo.setData(token); //return JSONUtil.toJsonStr(resultVo); return resultVo; } return null; } @AutoIdempotent @PostMapping("/test/Idempotence") public String testIdempotence() { Student student=new Student(); student.setSname("小白"); student.setSage(20); student.setSdept("test"); student.setSsex("male"); boolean save = studentService.save(student); if (save==true) { //ResultVo successResult = ResultVo.getSuccessResult(businessResult); //return JSONUtil.toJsonStr(successResult); return "添加成功"; } return "添加失败"; } }
代码编写完后,进行验证:
第一步发送请求获取token:
返回token,并且存储到redis中:
第二步,将token放到请求的Header中,发送处理业务逻辑的请求:
此时返回添加成功,并且Redis中token信息已被删除,在数据库中也成功添加一条数据:
第三步,发送重复请求,此时报500错误,后端出现异常:幂等性校验失败。
3.Redis分布式锁
单体锁存在的问题
在单体应用中,如果我们对共享数据不进行加锁操作,多线程操作共享数据时会出现数据一致性问题。
如下是一个下单问题:从redis中获取库存,检查库存是否够,>0才允许下单)
@PutMapping("/deduct_stock1") public String deductStock1(){ int total=Integer.parseInt((String) redisTemplate.opsForValue().get("goods:001")); if(total>0){ int realTotal=total-1; redisTemplate.opsForValue().set("goods:001",String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:"+realTotal+"件"); return "购买商品成功,库存还剩:"+realTotal+"件"; }else { return "购买商品失败,库存已无商品"; } }
使用Jmeter进行并发测试:
查看redis库存和后端控制台输出信息:
解决办法:加锁,加单体锁(synchronized或RentranLock)来保证单个实例并发安全。
@PutMapping("/deduct_stock2") public String deductStock2(){ synchronized (this){ int total=Integer.parseInt((String) redisTemplate.opsForValue().get("goods:001")); if(total>0){ int realTotal=total-1; redisTemplate.opsForValue().set("goods:001",String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:"+realTotal+"件"); return "购买商品成功,库存还剩:"+realTotal+"件"; }else { return "购买商品失败,库存已无商品"; } } }
一个tomocat实例是一个JVM进程,单体锁(synchronized、ReentrantLock)是JVM层面的锁,只能控制单个实例上的并发访问安全,多实例下依然存在数据一致性问题。
分布式锁
分布式锁指的是,所有服务中的所有线程都去获取同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,直到持有锁的线程释放锁。分布式锁是可以跨越多个实例,多个进程的锁。
分布式锁具备的条件:
- 互斥性:任意时刻,只能有一个客户端持有锁
- 锁超时释放:持有锁超时,可以释放,防止死锁
- 可重入性:一个线程获取了锁之后,可以再次对其请求加锁
- 高可用、高性能:加锁和解锁开销要尽可能低,同时保证高可用
- 安全性:锁只能被持有该锁的服务(或应用)释放。
- 容错性:在持有锁的服务崩溃时,锁仍能得到释放,避免死锁。
Redis实现分布式锁
方案一:set(key,value,nx,px)+uuid(存入value中,作为线程唯一标识)
@PutMapping("/deduct_stock3") public String deductStock3(){ String REDIS_LOCK="good_lock"; String value= UUID.randomUUID().toString().replace("-",""); //Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value); //给锁设置一个超时时间,到时自动释放锁(锁的过期时间大于业务执行时间) //redisTemplate.expire(REDIS_LOCK,10, TimeUnit.SECONDS); Boolean flag=redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value,1,TimeUnit.MINUTES); if(!flag){ return "抢锁失败"; } System.out.println(value+"抢锁成功"); int total=Integer.parseInt((String) redisTemplate.opsForValue().get("goods:001")); if(total>0){ int realTotal=total-1; redisTemplate.opsForValue().set("goods:001",String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:"+realTotal+"件"); return "购买商品成功,库存还剩:"+realTotal+"件"; }else { System.out.println("购买商品失败,库存已无商品"); } //谁加的锁谁才能删 if(redisTemplate.opsForValue().get(REDIS_LOCK).equals(value)){ redisTemplate.delete(REDIS_LOCK); } //redisTemplate.delete(REDIS_LOCK); return "购买商品失败,库存已无商品"; }
方案二:redis提供的实现分布式锁的第三方类库,Redisson。
@PutMapping("/deduct_stock5") public String deductStock5(){ String REDIS_LOCK="good_lock"; String value= UUID.randomUUID().toString().replace("-",""); RLock redissionLock=redisson.getLock(REDIS_LOCK); redissionLock.lock(); if(null==redissionLock){ return "抢锁失败"; } System.out.println(value+"抢锁成功"); int total=Integer.parseInt((String) redisTemplate.opsForValue().get("goods:001")); if(total>0){ int realTotal=total-1; redisTemplate.opsForValue().set("goods:001",String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:"+realTotal+"件"); return "购买商品成功,库存还剩:"+realTotal+"件"; }else { System.out.println("购买商品失败,库存已无商品"); } redissionLock.unlock(); return "购买商品失败,库存已无商品"; }
redis实现分布式锁总结:
方法1:set(key,value,nx,px)将setnx+setex变成原子操作。在value中存入uuid(线程唯一标识),删除锁时判断该标识,同时删除锁需保证原子性,否则还是有删除别人锁问题,可通过lredis事务释放锁。
方法2:利用redis提供的第三方类库,Redisson也可解决任务超时,锁自动释放问题。其通过开启另一个服务,后台进程定时检查持有锁的线程是否继续持有锁了,是将锁的生命周期重置到指定时间,即防止线程释放锁之前过期,所以将锁声明周期通过重置延长。
4.布隆过滤器的使用
1)什么是布隆过滤器
布隆过滤器(Bloom Filter)是 1970 年由布隆提出的,是一种非常节省空间的概率数据结构,运行速度快,占用内存小,但是有一定的误判率且无法删除元素。它实际上是一个很长的二进制向量或者位图(bitmap)和一系列随机映射函数组成,主要用于判断一个元素是否在一个集合中。
2)布隆过滤器的优缺点
布隆过滤器的优点:
- 支持海量数据场景下高效判断元素是否存在。
- 布隆过滤器存储空间小,并且节省空间,不存储数据本身,仅存储hash结果取模运算后的位标记。
- 不存储数据本身,比较适合某些保密。
布隆过滤器的缺点:
- 不存储数据本身,所以只能添加但不可删除,因为删掉元素会导致误判率增加。
- 由于存在hash碰撞,匹配结果如果是“存在于过滤器中”,实际不一定存在。
- 当容量快满时,hash碰撞的概率变大,插入、查询的错误率也就随之增加了。
3)Redis中配置布隆过滤器
参考:https://www.yisu.com/zixun/615274.html
4)Java集成Redis使用布隆过滤器
pom中引入redisson依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.1</version>
</dependency>
编写代码测试:
@RunWith(SpringRunner.class) @SpringBootTest(classes = BoyanRedisdemoApplication.class) public class TestBloomFilter2 { @Test public void TestBloom1(){ Config config = new Config(); SingleServerConfig singleServerConfig = config.useSingleServer(); singleServerConfig.setAddress("redis://192.168.169.131:6389"); // singleServerConfig.setPassword("123456"); RedissonClient redissonClient = Redisson.create(config); RBloomFilter<String> bloom = redissonClient.getBloomFilter("name"); // 初始化布隆过滤器; 大小:100000,误判率:0.01 bloom.tryInit(100000L, 0.01); // 新增10万条数据 for(int i=0;i<100000;i++) { bloom.add("name" + i); } // 判断不存在于布隆过滤器中的元素 List<String> notExistList = new ArrayList<>(); for(int i=0;i<100000;i++) { String str = "name" + i; boolean notExist = bloom.contains(str); if (notExist) { notExistList.add(str); } } if (notExistList!=null && notExistList.size() > 0 ) { System.out.println("误判次数:"+notExistList.size()); } } }