# 基于redis的一些实验 **Repository Path**: zoulizhi/bloom-filter ## Basic Information - **Project Name**: 基于redis的一些实验 - **Description**: 现在做了布隆过滤器,限流 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: https://gitee.com/zoulizhi/ - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-07-18 - **Last Updated**: 2025-02-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: 布隆过滤器, Redis ## README # 基于Redis的一些实验 ## 1 实验介绍 ### 1.1 布隆过滤器的实验 ### 1. 2 限流的实验 ### 1.3 软件架构 ```xml org.springframework.boot spring-boot-starter-parent 2.6.8 org.springframework.boot spring-boot-starter-webflux io.projectreactor reactor-test test org.springframework.cloud spring-cloud-starter-bootstrap 3.1.0 org.redisson redisson-spring-boot-starter 3.13.6 com.github.ulisesbocchio jasypt-spring-boot-starter 3.0.2 org.apache.commons commons-pool2 ``` ## 2布隆过滤器 ### 2.1介绍 为了防止缓存穿透,也就是缓存查不到,服务器也查不到,可以设置布隆过滤器,当然也可以直接将一些查不到的信息直接设置为空,这样也可以防止缓存穿透。布隆过滤器是一种概率性的查询,通过一个很大的Bitmap,然后将一个key经过hash运算,将bitmap对应的位置设置为1。后面如果有个Key打过来,这个key有可能不存在与bitmap,但是经过hash运算却存在,这种称为hash冲突。 ### 2.2配置 RedisSesionConfig ```java package com.bloomfiler.demo.config; import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.net.URISyntaxException; @Configuration public class RedisSesionConfig { @Value("${spring.redis.cluster.nodes}") String add; @Value("${spring.redis.password}") String pass; public Config config() throws URISyntaxException { System.out.println("ok"); System.out.println(add); System.out.println(pass); Config configs = new Config(); String[] adds = add.split(","); for (int i = 0; i < adds.length; i++) { adds[i] = "redis://" + adds[i]; } //#连接间隔 心跳 客户端长时间未使用,服务端会断开 configs.useClusterServers().addNodeAddress(adds).setClientName("rediss").setPassword(pass).setMasterConnectionPoolSize(40); return configs; } @Bean public RedissonClient redissonClient() throws URISyntaxException { RedissonClient redissonClient = Redisson.create(config()); return redissonClient; } @Bean public RLock rLock() throws URISyntaxException { return redissonClient().getLock("kk"); } } ``` RedissBloomFilerConfig ```java package com.bloomfiler.demo.config; import org.redisson.api.RBloomFilter; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; @Configuration public class RedissBloomFilerConfig { @Autowired RedissonClient redissonClient; @Bean public RBloomFilter bloomFilter(){ RBloomFilter bloomFilter =redissonClient.getBloomFilter("bloomtest"); //如果这里的容量不修改,下次重启服务,布隆过滤器不需要再次初始化,如果修改容量后,就有可能查不到,但是修改回来后,又可以,盲猜是在查询key存在的时候,会对当前容量取模。 bloomFilter.tryInit((long) 1E8,0.003); return bloomFilter; } } ``` 简单的放置数据和查询数据 ```java @RestController public class RedisController { @Autowired RedissonClient redissonClient; @Autowired RBloomFilter bloomFilter; @GetMapping("/add/{name}") public String addData(@PathVariable String name){ return bloomFilter.add(name)?"ok":"false"; } @GetMapping("/list/{name}") public boolean listData(@PathVariable String name){ return bloomFilter.contains(name); } } ``` ### 2.3结果 ![1658114591877](README.assets/1658114591877.png) ![1658114608277](README.assets/1658114608277.png) ![1658114622700](README.assets/1658114622700.png) ## 3 限流 ### 3.1 限流算法介绍 当前端大量请求打过来的时候,就需要我们用到限流了,限流的方式有:基于jdk;基于漏桶算法的;基于令牌桶算法的;基于nginx的;基于redis的。 ​ 基于jdk的方式只针对当前主机,但是使用简单; ​ 基于漏桶算法的,适用于匀速访问场景,不适用于有峰值变化的场景业务; ​ 基于令牌桶的算法就是定时颁发令牌,解决了漏桶算法的缺陷; ​ 基于nginx的限流,会将整个服务都给限流; ​ 基于redis的就是我们今天要做的,类似于令牌桶算法,定时颁发一定的令牌,这里我们主要实验的也是基于 redis的限流算法。 之所以要实验基于redis的限流算法, 是我在面试的时候,被某某面试官让我手撕限流业务算法,于是我就给他整了如下一段代码: ```java //限流场景 @GetMapping("/limit") public String streamLimitInterface() { int r = Integer.valueOf(StreamLimit("limit", "30", "1")); if (r > 0) { /** do SomeThing 业务代码 **/ return "ok" } return "当前访问繁忙,请稍后再试"; } ``` ```lua --- 用于限流的Lua脚本 if redis.call('exists',KEYS[1]) == 0 then redis.call('set',KEYS[1],ARGV[1]) redis.call('expire',KEYS[1],ARGV[2]) num = tonumber(redis.call('get',KEYS[1])) return num else if tonumber(redis.call('get',KEYS[1])) > 0 then redis.call('decrby',KEYS[1],1) return tonumber(redis.call('get',KEYS[1])) end end return 0 ``` ```java //该段代码用于调用lua脚本 @PostConstruct public void init1() { script = new DefaultRedisScript<>(); script.setResultType(Long.class); script.setScriptSource(new ResourceScriptSource(new ClassPathResource("Limit.lua"))); } public String StreamLimit(String gamekey, String maxStreamLimitNum, String TimeOut) { List keys = new ArrayList(); keys.add(gamekey); ExecutorService e; //script:lua脚本 //KEYS[1] KEYS[2],是要操作的键,可以指定多个,在lua脚本中通过KEYS[1], KEYS[2]获取 //ARGV[1] ARGV[2],参数,在lua脚本中通过ARGV[1], ARGV[2]获取 RedisSerializer redisSerializer = redisTemplate.getValueSerializer(); Object result = redisTemplate.execute(script, new StringRedisSerializer(), new StringRedisSerializer(), keys, maxStreamLimitNum, TimeOut); return result.toString(); } ``` 当时面试官一开始是让我针对我的订单服务写一个限流算法实现限流功能,于是我给她整出来了,谁知道后面她又问,你怎么判断临界条件,比如你现在只允许通过一个用户,但这个时候有多名用户来抢这个通过的权限,你怎么做。当时我就很诧异,lua脚本可以保证原子性,然后就是redis的单线程模型,这几个加起来已经可以保证redus不会出现超出我给定的QPS的情况, 我给她说了,但是她不认同,于是我开始扯就说面试官你的意思是让我再继续深入保证商品库存的一致性么,她还是让我解释那个临界值问题,我又给她解释了一遍,这下她不坚持了。后面又问了我其他问题,我也都答出来了,谁知道最后给我说时间不够了,本来想给我出道题。what?我就晓得她想挂我了,后面真把我挂了。 试验如下,lua脚本以及调用lua脚本的代码不改变,唯一改变的是,当返回结果小于0的时候,我们会把结果输出,并且每秒只允许5个用户通过。 ```java @GetMapping("/newlimit") public String newstreamLimitInterface() { int r = Integer.valueOf(StreamLimit("limit", "5", "1")); if (r < 0) { logger.info(String.valueOf(r)); } return "ok" + r; } ``` ### 3.2 试验结果 设置每秒产生20个用户 ![1660274289584](README.assets/1660274289584.png) ![1660274277081](README.assets/1660274277081.png) 结果上,循环了N次,也没有出现临界问题。唉,如果下次,我一定给她整个简单的限流算法,比如利用redis.zset,然后判断当前时间内一秒的用户有多少。 # 补充 redis的lua redistemplate一定要设置编码格式,不然获取的key会乱码。就像name11一样,前面会多出很多乱码字段。 ![1660053506973](README.assets/1660053506973.png) 解决乱码 ```java @Configuration public class myRedisTemlate { @Bean RedisTemplate redisTemplate( RedisTemplate redisTemplate){ redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } } ``` redis的lua在执行excute的时候,也要设置编码方式,不然整型无法识别,从源码里可以看到默认采用的是RedisSerializer,而我们传入的是String,如果不设置的话,执行redis.call('expire',KEYS[1],ARGV[2])会报错的。 ```java @Override public T execute(RedisScript script, RedisSerializer argsSerializer, RedisSerializer resultSerializer, List keys, Object... args) { return scriptExecutor.execute(script, argsSerializer, resultSerializer, keys, args); } Object result = redisTemplate.execute(script,new StringRedisSerializer(),new StringRedisSerializer(), keys, curtime,num); ``` #### 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request #### 特技 1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md 2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) 3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) 6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)