目录
- 一、Redis数据结构和常用命令
- 1、Redis简介
- 2、Redis应用场景
- 3、Redis数据结构与常用命令
- 4、Redis使用案例
- 二、Redis持久化机制
- 三、Redis内存管理
- 四、Redis主从复制
- 五、Redis哨兵高可用机制
- 六、Redis集群分片存储机制
一、Redis数据结构和常用命令
1、Redis简介
Redis是一个开源的C语言编写、支持网络、可基于内存亦可持久化的日志型,key-value数据库,并提供多种语言API。它的特点是使用简单、性能强悍以及功能应用场景丰富。(我们可以简单把它理解为一个HashMap)
2、Redis应用场景
Redis应用场景包括以下几种:
(1)缓存:最常用的场景;
(2)消息队列:Redis有以下几种消息队列实现方式:
<1>简单消息队列:利用List数据结构(lpush/rpop,rpush/lpop),生产者在一头塞入数据,消费者另一条取出数据(不可持久化);
<2>发布订阅模式:对某个key发布消息,则订阅此key的程序即可接收到消息或者对于此key的操作(没有确认机制);
<3>Redis5.0开始支持Stream,可以认为是一种MQ实现,类似于Kafka;
(3)分布式锁:利用Redis的单线程特点,某一时间只有一个线程可以执行对某个key的修改;
(4)限流:利用Redis的计数器功能控制某段时间内的请求数量,但是不能保证请求的流量是平滑的;
(5)服务发现:类似于Zookeeper的功能;
对于Redis一些潜在问题与解决方案如下:
(1)缓存雪崩:高并发场景下,如果redis宕机不可用,次数大量的请求流量打到数据库上,造成整个系统“雪崩”。(解决方案:可以通过主从+哨兵或者redis cluster来提供高可用;或者采用一些降级机制,如hystrix,以保护数据库)
(2)缓存穿透:对于某个根本不可能存在于缓存中的key进行大量访问,导致“穿透”了缓存,而流量直接打到了数据库上,造成系统压力。(解决方案:可以用布隆过滤器,先过滤掉不可能存在的key)
(3)缓存击穿:某些非常热点的key,访问十分频繁,但是突然这些key过期了,造成大量请求流量打到了数据库上,“击穿”了缓存。(解决方案:把热点key的过期时间错开或永不过期;或者加上互斥锁,只让一个线程去数据库里访问,得到数据后再重建缓存,之后的线程直接就能从缓存中取到数据)
(4)缓存一致性:因为更新数据库和Redis不是一个原子性操作,会造成缓存“不一致”。(解决方案:根据具体业务定,可以选择定时任务同步数据的方式,类似分布式事务的本地消息表的解决方案;或者先更新数据库在删除缓存(修改和查询Redis并发场景可以,不过会短时间内不一致(这种方式个别情况下还是会不一致));或者利用canal异步同步数据库和Redis的数据)
3、Redis数据结构与常用命令
Redis有以下7种数据结构:
(1)String:简单的k-v类型,value其实不仅是String,也可以是数字(场景:键值对存储)
常用命令如下:
(2)List:即链表结构,有顺序,可重复(场景:关注列表)
常用命令如下:
(3)Set:即一个无序不重复集合(场景:存储集合性数据)
常用命令如下:
(4)Sort Set:与Set类型,但是有顺序,用户需提供额外的优先级(score)参数为元素排序(场景:排行榜)
常用命令如下:
(5)Hash:是一个String类型的field和value的映射表,和HashMap类似(场景:存储用户信息)
常用命令如下:
(6)GEO:Redis3.2开始对GEO(地理位置)支持(场景:LBS应用开发)
常用命令如下:
(7)Stream:Redis5.0开始的新结构“流”(场景:消息队列)
常用命令如下:
4、Redis使用案例
(1)用List实现消息队列:
public void list() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.auth("123");
// 插入数据1 --- 2 --- 3
jedis.rpush("queue_1", "1");
jedis.rpush("queue_1", "2", "3");
List<String> strings = jedis.lrange("queue_1", 0, -1);
for (String string : strings) {
System.out.println("目前队列中为:"+string+"\n");
}
// 消费者线程简例
while (true) {
String item = jedis.lpop("queue_1");
if (item == null) break;
System.out.println("消费了:"+item+"\n");
}
jedis.close();
}
(2)用Hash存储对象:
public void hashTest() {
HashMap<String, Object> user = new HashMap<>();
user.put("name", "tony");
user.put("age", 18);
user.put("userId", 10001);
System.out.println(user);
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.auth("123");
jedis.hset("user_10001", "name", "tony");
jedis.hset("user_10001", "age", "18");
jedis.hset("user_10001", "userId", "10001");
System.out.println("redis版本~~~~~");
// jedis.hget("user_10001", "name");
System.out.println(jedis.hgetAll("user_10001"));
jedis.close();
}
(3)用Set实现交集/并集:
public void setTest() {
// 取出两个人共同关注的好友
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.auth("123");
// 每个人维护一个set
jedis.sadd("user_A", "userC", "userD", "userE");
jedis.sadd("user_B", "userC", "userE", "userF");
// 取出共同关注
Set<String> sinter = jedis.sinter("user_A", "user_B");
System.out.println(sinter);
// 检索给某一个帖子点赞/转发的
jedis.sadd("trs_tp_1001", "userC", "userD", "userE");
jedis.sadd("star_tp_1001", "userE", "userF");
// 取出共同人群
Set<String> union = jedis.sunion("star_tp_1001", "trs_tp_1001");
System.out.println(union);
jedis.close();
}
(4)用SortSet实现排行榜:
public void zsetTest() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.auth("123");
String ranksKeyName = "exam_rank";
jedis.zadd(ranksKeyName, 100.0, "tony");
jedis.zadd(ranksKeyName, 82.0, "allen");
jedis.zadd(ranksKeyName, 90, "mengmeng");
jedis.zadd(ranksKeyName, 96, "netease");
jedis.zadd(ranksKeyName, 89, "ali");
Set<String> stringSet = jedis.zrevrange(ranksKeyName, 0, 2);
System.out.println("返回前三名:");
for (String s : stringSet) {
System.out.println(s);
}
Long zcount = jedis.zcount(ranksKeyName, 85, 100);
System.out.println("超过85分的数量 " + zcount);
jedis.close();
}
(5)用pipline批量操作:
public void test1() throws InterruptedException {
// 普通模式和pipeline模式
long time = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
redisTemplate.opsForList().leftPush("queue_1", i);
}
System.out.println("操作完毕:" + redisTemplate.opsForList().size("queue_1"));
System.out.println("普通模式一百次操作耗时:" + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
redisTemplate.executePipelined(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
for (int i = 0; i < 10000; i++) {
connection.lPush("queue_2".getBytes(), String.valueOf(i).getBytes());
}
return null;
}
});
System.out.println("操作完毕:" + redisTemplate.opsForList().size("queue_2"));
System.out.println("pipeline一万次操作耗时:" + (System.currentTimeMillis() - time));
}
(6)用GEO实现附近的人:
public void test1() throws InterruptedException {
// 模拟三个人位置上报
geoExampleService.add(new Point(116.405285, 39.904989), "allen");
geoExampleService.add(new Point(116.405265, 39.904969), "mike");
geoExampleService.add(new Point(116.405315, 39.904999), "tony");
// tony查找附近的人
GeoResults<RedisGeoCommands.GeoLocation> geoResults = geoExampleService.near(new Point(116.405315, 39.904999));
for (GeoResult<RedisGeoCommands.GeoLocation> geoResult : geoResults) {
RedisGeoCommands.GeoLocation content = geoResult.getContent();
System.out.println(content.getName() + " :" + geoResult.getDistance().getValue());
}
}
(7)实现发布订阅的消息队列:
public void test1() throws InterruptedException {
System.out.println("开始测试发布订阅机制,5秒后发布一条消息");
Thread.sleep(5000L);
redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
// 发送通知
Long received = connection.publish(PubsubRedisAppConfig.TEST_CHANNEL_NAME.getBytes(), "{手机号码10086~短信内容~~}".getBytes());
return received;
}
});
}
// 隐藏功能~~黑科技~~当key被删除,或者key过期之后,也会有通知~
public void test2() throws InterruptedException {
redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
connection.subscribe((message, pattern) -> {
System.out.println("收到消息,使用redisTemplate收到的:" + message);
}, "__keyevent@0__:del".getBytes());
return null;
}
});
redisTemplate.opsForValue().set("hkkkk", "tony");
Thread.sleep(1000L);
redisTemplate.delete("hkkkk");
}
二、Redis持久化机制
Redis持久化(需要开启)即将数据保存到硬盘上,当Redis重启后可以从磁盘中恢复。Redis持久化分为以下2种:
(1)RDB(操作结果记录下来):能够在指定时间间隔对数据进行快照存储,重启时恢复数据。可通过以下命令来创建一个内存快照:
<1>BGSAVE:fork一个子进程,负责将快照写入磁盘,而父进程仍继续处理命令;
<2>SAVE:执行SAVE命令过程中不再响应其他命令。
在redis.conf中调整Save选项,在规定时间内,Redis发生写操作的次数满足条件会触发BGSAVE:
RDB的优缺点如下:
(2)AOF(操作命令记录下来):记录每次对服务器的写操作,重启时重新执行一遍。用“appendonly yes”开启AOF;调整策略如下:
AOF的优缺点如下:
三、Redis内存管理
Redis存储主要是在内存,Redis对不同类型进行了大小的限制,如下:
Redis为了节省内存,会对内存进行压缩,例如以下设置:
对于以上配置,当小于设置的数值时会压缩,当大于设置的数值时则不会压缩。
对于过期数据的处理策略如下:
(1)主动处理:Redis主动检测key是否过期,每秒执行10次,过程如下:
<1>从具有相关过期的key集中测试20个随机key;
<2>删除已过期的key;
<3>若超过25%的key已过期,则从步骤1重新开始。
(2)被动处理:每次访问key时,发现已过期的key后被动删除。
对于数据恢复阶段过期数据的处理策略如下:
(1)RDB方式:已过期的key不会被持久化到文件中;恢复时发现过期的key,会通过Redis主动和被动方式清理掉;(加载数据进来再删除过期key)
(2)AOF方式:恢复时发现过期的key,Redis会追加一条DEL命令到AOF文件,所有顺序加载AOF命令时会删掉过期的key。(加载命令时直接删除过期key)
对于内存回收策略如下:(即内存不够时继续插入数据就会触发内存回收)
对于LRU算法说明如下:(最近没访问的就删除)
对于LFU算法说明如下:(历史访问频率小的就删除)
四、Redis主从复制
由于单台Redis服务器有故障的风险,并且单台Redis的性能有限;我们会使用Redis主从复制。Redis主从复制适用于读写分离和故障切换的场景。主从复制流程如下:
Redis默认使用异步复制,slave和master之间异步确认处理的数据量,一个master可以接受多个slave,slave也可以接受其他slave的连接,slave也可以有下级的subslave。主从复制的过程中,在master侧是非阻塞的;而slave侧是阻塞的不会接收其他请求。
Redis主从复制有如下的注意事项:
五、Redis哨兵高可用机制
对于上面讲的Redis主从复制,出现问题时我们需要手动的切换master;所以就出现了Redis哨兵机制来帮我们自动监控、提醒和故障转移,以保证Redis的高可用。哨兵机制图示如下:
如上图所示,哨兵负责监控master节点的状态,客户端第一次连接到Redis时无需知道Redis节点的地址而是直接连接哨兵以找到master节点;当客户端连接上master节点时,就可以与master交互了,此时几遍哨兵突然宕机也并不会影响客户端使用Redis(因为现在master正常使用);只有当master节点宕机,客户端发现找不到master节点,它才会向哨兵要当前的master节点是谁。而当一定数量(可设置)的哨兵主观认为master节点有问题时,哨兵才会基于Raft算法去选举一个执行故障切换的哨兵,为Redis执行主从切换。
哨兵机制的核心运作流程如下:
哨兵启动和配置如下:
接下来我们来明确关于哨兵机制的7大核心问题:
(1)哨兵如何知道Redis的主从消息?
(2)什么是master主观下线?
(3)什么是客观下线?
(4)哨兵之间如何通讯?
(5)哨兵如何选举领导?
【补充】Raft算法示例:Raft算法可视化示例(6)slave节点的选举方案?(下列标准从上到下依次筛选)
(7)最终的主从切换过程?
【注】为保证高可用,建议哨兵最好部署3个。
六、Redis集群分片存储机制
上述的主从复制机制和哨兵机制的写操作实际上还是单台Redis,而当缓存数据量非常大时,单台Redis已经不能支撑,所以此时就需要Redis的分片存储机制。Redis Cluster是Redis的分布式集群解决方案,在3.0版本推出后有效地解决了Redis分布式方面的需求,实现了数据在多Redis节点间自动分片、故障自动转移和扩容机制等功能。
Redis集群分片存储机制图示如下:(类似于HashMap的存储方式)
如上图所示,Redis一共预设了16384个slot(“槽”),Redis可以算出每个key应该对应于哪个slot(如0到5461的slot在节点0上,依次类推),即此时Redis(各个节点)知道key应该存放在哪个节点上。而当客户端连接Redis Cluster并存入数据时,它并不知道自己的key应该存入哪个节点,那么客户端会随机连接上一个Redis节点,如果正好是正确的节点则存储成功;如果是错误的节点则Redis节点“告知”客户端应该存放的正确节点,然后客户端“重定向”把数据存放在正确的节点上。(为了防止客户端大量的访问到错误的节点,客户端本身可以定时刷新或在收到重定向的响应后去更新集群中slot的分配信息)
接下来我们来明确关于集群分片存储机制的几大核心问题:
(1)增加了slot槽的计算,是不是比单机性能差?
(2)Redis集群大小,可以存储多少数据?
(3)集群节点间是如何通讯的?
(4)ask和moved重定向的区别?
(5)数据倾斜和访问倾斜的问题?
(6)slot手动迁移怎么做?
(7)节点间会交换信息,带来的贷款的损耗?
(8)pub/sub发布订阅机制?
(9)读写分离?(从节点只用来做主节点的备份以保持高可用,虽然也可以设置读写分离)
【注】当Redis节点增加或减少时,可以采用一致性Hash算法来保证缓存查询的命中率。
【补充】Redis集群搭建:Redis集群搭建 提取码:xxg5