这里列举常见的redis实战场景
大概从网上找来了下面这里实战场景:
1、最新20条评论
2、排行榜
3、计数
4、统计在某段特点时间里有多少特定用户访问了某个特定资源(比如我想要知道某些特定的注册用户或IP地址,他们到底有多少访问了某篇文章。)
5、按照用户投票和时间排序
6、实时分析正在发生的情况,用于数据统计与防止垃圾邮件等
7、Reverse cache(反向cache)
8、用户最近访问记录
9、Fast transaction with Lua
Redis 的Lua的功能扩展实际给Redis带来了更多的应用场景,你可以编写若干command组合作为一个小型的非阻塞事务或者更新逻辑,如:在收到 message推送时,同时1.给自己的增加一个未读的对话 2.给自己的私信增加一个未读消息 3.最后给发送人回执一个完成推送消息,这一层逻辑完全可以在Redis Server端实现。
但是,需要注意的是Redis会将lua script的全部内容记录在aof和传送给slave,这也将是对磁盘,网卡一个不小的开销。
10、分布式锁
11、抢号(不一定每人都有),抢红包模式(每人都有)
12、Uniq操作,获取某段时间所有数据去重值
13、打卡
14、session存储
15、bitmap的使用场景
16、文章投票
17、限流工具18、好友关系
19、热数据查询
具体api说明:https://blog.csdn.net/sinat_22797429/article/details/89196933
1、排行榜场景(利用zset)
需求:
前段时间,做了一个世界杯竞猜积分排行榜。对世界杯64场球赛胜负平进行猜测,猜对+1分,错误+0分,一人一场只能猜一次。
1.展示前一百名列表。
2.展示个人排名(如:张三,您当前的排名106579)。一开始打算直接使用mysql数据库来做,遇到一个问题,每个人的分数都会变化,如何能够获取到个人的排名呢?数据库可以通过分数进行row_num排序,但是这个方法需要进行全表扫描,当参与的人数达到10000的时候查询就非常慢了。
redis的排行榜功能就完美锲合了这个需求。来看看我是怎么实现的吧。实现逻辑:
redis中的zset(sorted sets)数据类型
Sorted Sets数据类型就像是set和hash的混合。与sets一样,Sorted Sets是唯一的,不重复的字符串组成。可以说Sorted Sets也是Sets的一种。
Sorted Sets是通过Skip List(跳跃表)和hash Table(哈希表)的双端口数据结构实现的,因此每次添加元素时,Redis都会执行O(log(N))操作。所以当我们要求排序的时候,Redis根本不需要做任何工作了,早已经全部排好序了。元素的分数可以随时更新。
将数据以zset的数据方式存进redis(根据得分为score属性)。在存进去的时候redis已经帮我们排序好了。、
(存数据时以分数为score属性,在获取时根据score属性值进行逆序获取)
1.展示前一百名列表。
代码实现:
依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.60</version> </dependency> </dependencies>
创建以下结构:
controller:
@RestController public class PhbController { @Autowired PhbServiceImpl phbServiceImpl; @GetMapping("/phb") public void phb(){ phbServiceImpl.phbImpl(); } }
service:
@Service public class PhbServiceImpl { //注入template对象(也可以直接用RedisTemplate) //注意StringRedisTemplate表示key、value都是String类型,如果用RedisTemplate则key为String、value为object类型 @Autowired StringRedisTemplate redisTemplate; //redis存储的key值 public static final String SCORE_RANK = "score_rank"; //实现逻辑 public void phbImpl(){ //1、模拟10万条数据 Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>(); long start = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) {//向set集合中插入10万条数据 DefaultTypedTuple<String> tuple = new DefaultTypedTuple<>("张三" + i, 1D + i); tuples.add(tuple); } System.out.println("循环时间:" +( System.currentTimeMillis() - start)); //2、向redis中添加该set集合 //此时的redis中数据则是key:SCORE_RANK----value:{("张三" + i,1D + i),("张三" + i,1D + i)} //其中以1D + i为score属性进行排序 Long num = redisTemplate.opsForZSet().add(SCORE_RANK, tuples); System.out.println("批量新增时间:" +(System.currentTimeMillis() - start)); System.out.println("受影响行数:" + num); //3、获取排行版前10名列表+获取前10名分数和名称列表 list(10); } private void list(int count){ //获取排名列表 Set<String> range = redisTemplate.opsForZSet().range(SCORE_RANK, 0, count); System.out.println("获取到的排名列表为:"+range); //获取排行和分数列表 Set<ZSetOperations.TypedTuple<String>> rangeWithScores = redisTemplate.opsForZSet().reverseRangeWithScores(SCORE_RANK, 0, 10); System.out.println("获取排行和分数列表为:"+ JSON.toJSONString(rangeWithScores)); } }
启动类:
@SpringBootApplication public class phbApplication { public static void main(String[] args) { SpringApplication.run(phbApplication.class,args); } }
yml文件:
spring: #redis配置信息 redis: #redis 服务器地址 host: 192.168.211.20 #redis端口 port: 6379 #客户端超时时间单位是毫秒 默认是2000 timeout: 5000 #最大空闲数 maxIdle: 20 #连接池的最大数据库连接数 maxActive: -1 #控制一个pool可分配多少个jedis实例,用来替换上面的maxActive maxTotal: 100 #最大建立连接等待时间。如果超过此时间将接到异常 maxWaitMillis: 100 #连接的最小空闲时间 minEvictableIdleTimeMillis: 864000000 #每次释放连接的最大数目 numTestsPerEvictionRun: 10 #逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程 timeBetweenEvictionRunsMillis: 300000 #是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个 testOnBorrow: true #在空闲时检查有效性 testWhileIdle: false #数据库 database: 0 server: port: 9090
访问:http://localhost:9090/phb接口查看控制台:
上面是进行所有人员进行排序的,如果此时只想获取某个人的分数和排名,如何实现?
2.展示个人排名(如:张三,您当前的排名106579)。
代码实现:
在phbImpl方法中先模拟一个数据的插入:
redisTemplate.opsForZSet().add(SCORE_RANK, "李四", 8899);
在list方法中打印该人的排位等:
Long rankNum = redisTemplate.opsForZSet().reverseRank(SCORE_RANK, "李四"); System.out.println("李四的个人排名:" + rankNum); Double score = redisTemplate.opsForZSet().score(SCORE_RANK, "李四"); System.out.println("李四的分数:" + score);
访问http://localhost:9090/phb查看控制台:
3、统计某分数区间有多少人?
代码实现:8001-9000区间人数
Long count = redisTemplate.opsForZSet().count(SCORE_RANK, 8001, 9000);
4、获取整个集合数量
Long aLong = redisTemplate.opsForZSet().zCard(SCORE_RANK);
5、对某个人进行分数修改,使用加法操作分数(将李四的值加1000)
Double score = redisTemplate.opsForZSet().incrementScore(SCORE_RANK, "李四", 1000);
(上面修改时如果有缓存数据则为更新,没有则为新增)
6、总结:
1)、新增or更新
有三种方式,一种是单个,一种是批量,对分数使用加法(如果不存在,则从0开始加)。
//单个新增or更新 Boolean add(K key, V value, double score); //批量新增or更新 Long add(K key, Set<TypedTuple<V>> tuples); //使用加法操作分数 Double incrementScore(K key, V value, double delta);
2)、删除:删除提供了三种方式:通过key/values删除,通过排名区间删除,通过分数区间删除。
//通过key/value删除 Long remove(K key, Object... values); //通过排名区间删除 Long removeRange(K key, long start, long end); //通过分数区间删除 Long removeRangeByScore(K key, double min, double max);
3)查询:
3.1:列表查询:分为两大类,正序和逆序。以下只列表正序的,逆序的只需在方法前加上reverse即可:
//通过排名区间获取列表值集合
Set<V> range(K key, long start, long end); //通过排名区间获取列表值和分数集合 Set<TypedTuple<V>> rangeWithScores(K key, long start, long end); //通过分数区间获取列表值集合 Set<V> rangeByScore(K key, double min, double max); //通过分数区间获取列表值和分数集合 Set<TypedTuple<V>> rangeByScoreWithScores(K key, double min, double max); //通过Range对象删选再获取集合排行 Set<V> rangeByLex(K key, Range range); //通过Range对象删选再获取limit数量的集合排行 Set<V> rangeByLex(K key, Range range, Limit limit);
3.2:单人查询
可获取单人排行,和通过key/value获取分数。以下只列表正序的,逆序的只需在方法前加上reverse即可:
//获取个人排行 Long rank(K key, Object o); //获取个人分数 Double score(K key, Object o);
4)、统计:
统计分数区间的人数,统计集合基数。
//统计分数区间的人数 Long count(K key, double min, double max); //统计集合基数 Long zCard(K key);
以上就是redis中使用排行榜功能的一些例子,和对redis的操作方法了。redis不仅仅只是作为缓存,它更是数据库,提供了许多的功能,我们都可以好好的利用。
在这里我使用redis来实现了世界杯积分排行的展示,无论是在批量更新或是获取个人排行等方便,都有着很高效率,也降低了对数据库操作的压力,达到了很好的效果。
2、最新20条评论(利用list)
关注列表,粉丝列表,评论列表等都可以用Redis的list结构来实现。这里使用lpush和ltrim简单实现始终取最新评论的前20条。
//模拟从左边插入两条数据 redisTemplate.opsForList().leftPush("commons:id001","id为001的文章第1条评论"); redisTemplate.opsForList().leftPush("commons:id001","id为001的文章第2条评论"); //获取最新的一条数据(0-0区间) List<String> range1 = redisTemplate.opsForList().range("commons:id001", 0, 0); System.out.println(range1);
查看控制台:
list类型还可以用来其他操作:
3、计数器(限流)
如果要记录某个ip或用户的请求次数,限制一天请求次数不超过2次,超过则进行限制访问
实现:在redis中存相应用户的缓存,key为用户标示,value为访问次数,每次访问则利用incr自增1,每次访问都查看访问次数是否超过了2次,超过了则进行限流。
这里不做具体代码,就写几个命令代码说明下:
//缓存一个值 redisTemplate.opsForValue().set("bbb","100"); //该值加上100 redisTemplate.opsForValue().increment("bbb",100); System.out.println(JSON.toJSONString(redisTemplate.opsForValue().get("bbb")));
最后输出为“200”。注意这里的key,value都是String类型,虽然用inc方法时String和int类型相加后为200,但200仍然是String类型。那为什么value也是String类型的呢?因为我们在之前注入的redisTemplate是注入了StringRedisTemplate,所以其key,value都是String类型,如果此时换成RedisTemplate类型,此时key为String,value则为object类型,即可进行正常的incr操作了。或者可以使用下面这种方式:将key设置为限流标识,value为用户标识,访问次数为score,对其修改则是对score值进行修改(上面有说到如何对score值进行修改)
也可以用分布式信号量实现
4、获取某段时间所有数据去重值
存进set即可
5、打卡签到
可以用hash、set,但为了节省空间可以用bitmap。当然也可以用mysql实现
6、session存储
即springsession实现方案,在分布式系统中不同机器有不同session,此时要将session统一放在某个数据库中,此时就能实现分布式统一共享session,session共享可以将session放到redis、tomcat、mysql等中进行统一存储管理。这里将session内容存到redis中进行共享。
springsession在一定程度上可以解决单点登陆问题,前提是所有系统是父子域名关系,因为利用cookie实现单点登陆的方案中,每个cookie都有一个对应的域,该域对应相应的域名。springsession中可以将cookie中token的访问域设置为父子域都可访问,则解决了单点登陆的问题。
这里就以简单的session获取存储为例进行说明springsession的实现
//官方案例:https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html
1、依赖:
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
2、配置springsession相关配置:
这里也可以不写,后面用@EnableRedisHttpSession注解代替
#表示将session移交存储到redis session: store-type: redis
3、代码实现:
3.1、在启动类上添加@EnableRedisHttpSession注解,开启springsession功能。
@EnableRedisHttpSession @SpringBootApplication public class phbApplication { public static void main(String[] args) { SpringApplication.run(phbApplication.class,args); } }
(注意这里如果要向redis存一些对象数据,该对象要实现序列化接口seri,因为向redis存数据是要进行序列化的,java对象默认是jdk序列化,所以要对象实现序列化接口)
3.2、完整代码实现:
@RestController public class SessionDemo { @GetMapping("/session") public void sessionDemo(HttpServletRequest request, HttpServletResponse response){ HttpSession session = request.getSession(); session.setAttribute("sessionDemo",123123); System.out.println(session.getAttribute("sessionDemo")); } }
8、好友关系
求交集
set类型中:
获取两个或者多个集合的并集(otherKeys可以为单个值或者是集合)
redisTemplate.opsForSet().union(key, otherKeys)
获取两个集合的交集(key对应的无序集合与otherKey对应的无序集合求交集)
redisTemplate.opsForSet().intersect(key, otherKey)
10、bitmap的使用场景
1、打卡签到
2、统计活跃用户
3、用户在线状态
一般状态只存在两种情况时可用位图(bitmap),如签到只有签到和不签到两种状态。
用bitmap不用其他数据类型的比较:https://www.cnblogs.com/spareyaya/p/12806637.html
11、分布式锁
实现方式:
1、使用jedis/springdata-redis+lua实现
2、使用redisson实现(底层也是用lua脚本实现)
3、zookeeper实现
4、数据库
因为第一种方法要实现比较复杂,而redission为我们封装了一轮代码,直接为我们提供watchedog、分布式锁等实现机制,所以一般情况下redis的分布式锁的实现都是用redission实现比较简单。
redission实现redis的分布式锁:(redisson也实现了juc接口,所以使用基本和juc类似)
这里以redisson官方文档为主https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95
1、依赖:
<!--redisson分布式锁--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.5.0</version> </dependency>
2、redisson配置(可以用程序配置,或者yml配置然后再加载)(如果依赖是sprinboot-data-redisson的包,则可直接在yml配置)这里使用程序配置
2.1、向容器注入redis的配置类MyRedisConfig
@Configuration public class MyRedisConfig { /** * 所有操作都是通过redissonClient对象操作 * * */ @Bean(destroyMethod = "shutdown")//执行完后的销毁方法shutdown public RedissonClient redisson() throws Exception{ //1、配置信息 Config config = new Config(); //集群配置 config.useClusterServers() .addNodeAddress("redis://192.168.211.20:6379", "redis://192.168.211.20:6380", "redis://192.168.211.20:6381"); //根据配置信息创建出redissonClient return Redisson.create(config); } }
3、此时就可以直接来使用redisson
基于Redis的Redisson分布式可重入锁
RLock
Java对象实现了java.util.concurrent.locks.Lock
接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。所有redisson的分布式锁和同步器的使用类似于juc锁的使用
这里基于Reentrant Lock和Semaphore进行举例
3.1、Reentrant Lock
@RestController public class RedisLock { @Autowired RedissonClient redissonClient; @GetMapping("/lock") public void lock(){ RLock lock = redissonClient.getLock("myLock"); // 最常见的使用方法,阻塞式等待 lock.lock(); try { System.out.println("加锁成功,执行业务......."+Thread.currentThread().getId()); Thread.sleep(30000);//模拟业务30秒 } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); System.out.println("释放锁...."+Thread.currentThread().getId()); } } }
此时在浏览器开启两个访问端口,都发送请求,此时只有68线程获取到锁,在30秒后释放锁,再被另一个请求获取到锁
此时除了这些方法还存在其方法,例如:
// 加锁以后10秒钟自动解锁 // 无需调用unlock方法手动解锁 lock.lock(10, TimeUnit.SECONDS); // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); //异步执行 lock.lockAsync(); lock.lockAsync(10, TimeUnit.SECONDS); Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
3.2 Semaphore的使用举例
注意:一个信号量只能指定一次许可数量trySetPermits(再次使用无效),要表可以使用reducePermits/drainPermits/手动去redis修改相应的值
RSemaphore semaphore = redissonClient.getSemaphore("semaphore"); semaphore.trySetPermits(5);//设置有5个坑位(5个许可) //加锁、获取一个坑位 semaphore.acquire(); //解锁 semaphore.release();
另外还有其他方法:
semaphore.acquireAsync(); semaphore.acquire(23);//获取23个坑位 semaphore.tryAcquire(); //或 semaphore.tryAcquireAsync(); semaphore.tryAcquire(23, TimeUnit.SECONDS);//获取一个坑位,等待时间为23秒 //或 semaphore.tryAcquireAsync(23, TimeUnit.SECONDS); semaphore.release(10); semaphore.release(); //或 semaphore.releaseAsync();
public interface RSemaphore extends RExpirable, RSemaphoreAsync { boolean trySetPermits(int var1); //设置permits数 void reducePermits(int var1); //减少permit数 void acquire() throws InterruptedException; //获得一个permit void acquire(int var1) throws InterruptedException; //获得var1个permits boolean tryAcquire(); //尝试获得permit boolean tryAcquire(int var1); //尝试获得var1个permit boolean tryAcquire(long var1, TimeUnit var3) throws InterruptedException; //尝试获得permit,等待时间var1 boolean tryAcquire(int var1, long var2, TimeUnit var4) throws InterruptedException; //尝试获得var1个permit,等待时间var2 void release(); //释放permit void release(int var1); //释放var1个permits int availablePermits(); //信号量的permits数 int drainPermits(); //清空permits }
信号量可以用来实现限流操作(同时允许n个线程访问)
12、发布订阅功能
https://blog.csdn.net/wlddhj/article/details/107006035
https://zhuanlan.zhihu.com/p/59065399
由于没有消息持久化与 ACK 的保证,所以,Redis 的发布订阅功能并不可靠。这也就导致了它的应用场景很有限,建议用于实时与可靠性要求不高的场景。
13、抢号(不一定每人都有),抢红包模式
使用redisson实现分布式场景(因为获取到锁+减库存的全过程要求是原子操作的)
抢号:可以使用redisson的ReentrantLock锁,一个人获取到锁就减一个号
抢红包:也可以用分布式锁,指定抢红包人数,随机分成n份数额,一个人获取到锁就红包数减1并且拿走随机的一个金额。
14、延时队列
利用zset中有score的属性,以时间为score属性,这样zset会以时间前后进行排序,然后消费者再利用线程或者定时任务去查看当前时间大于哪个zset中的元素则拿出来消费。(不用遍历,大于就拿来消费即可)
15、布隆过滤器
主要用来解决redis的缓存穿透问题。
其中谷歌的guava则封装了布隆过滤器的实现,但那是把数据放在本地内存中,如果要将数据放在redis中,redis也有实现的一个布隆过滤器插件,需要去安装使用。
16、redis在高并发中的缓存穿透,缓存击穿,缓存雪崩实例解决
穿透:设null值/布隆过滤器
击穿、雪崩:均匀分布过期时间,限流,热数据,预加载等操作
具体实现后面再说