这里列举常见的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
(官方文档:https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8)
基于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值/布隆过滤器
击穿、雪崩:均匀分布过期时间,限流,热数据,预加载等操作
具体实现后面再说
|