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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值