Redis知识点全面汇总

系列文章目录



前言

提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、数据结构

1.字符串

Redis的数据结构中,字符串主要用来存储一些String类型的数据,Redis中实现字符串类型是通过SDS来实现的,SDS是一个动态字符串类型。在Redis中字符串类型长度以及字符串扩容则是通过定义的对象来实现的,在Redis中定义了字符串对象,这个对象中主要存储的数据为字符串长度,未使用字符串容量,以及字符串数据等。

因此在Redis中获取字符串长度的时间复杂度是O1,同时为了解决存储可变字符串的问题,Redis则使用了动态扩容来解决,当Redis向一个字符串中新增字串时,则会判断字符串的容量是否足够,如果容量不足以存储新增的字串,那么Redis则对字符串执行空间预分配策略,也就是当SDS为字符串分配空间时,同时会为SDS分配一个未使用字符串容量,这个容量可以在后续中为新增字符串时做铺垫。

如果SDS中存储的数据容量长度为10字节,同时Redis分配了10字节未使用的空间,那么SDS的总长度就为10+10+1=21,之所以最后加1是因为Redis遵循C语言的准则在末尾添加一个空字节。

2.链表

Redis中的链表结构跟数据结构中的链表大体相同。

3.字典

字典在Redis中就是我们平时使用Java中的map,有key-value形式的数据结构,但是这个结构在Redis中的实现有一些不同,首先Redis中是通过一个table属性来保存key-value的值,table属性是一个数组,这个数组的类型是dictEntry类型,这个类型就是我们所说的key-value。

Redis中也是通过哈希算法来进行存储字典值的,既然使用了哈希算法,那么也不可避免的要出现哈希冲突的问题,也就是两个或以上的键被分配到了一个数组上。对于这种情况Redis则使用了链地址法来解决,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单项链表,被分配到同一个索引上的多个节点可以通过这个单向链表连接起来。

4.跳跃表

Redis中的ZSet的底层实现就是通过跳跃表来实现的,通过跳跃表来实现有序集合。

跳跃表的实现基于一种特殊的数据结构,这种结构的时间复杂度为Ologn,这里可以跟平衡树做个对比,在平衡树中查找一个数的时候所走过的路径是从根部向下搜索,直到找到这个数据,如果数据过多,那么查找路径就会边长,影响一定的性能,而跳跃表则很好的解决了这个问题。

跳跃表有几个概念,层、长度、头部、尾部、分值、成员对象,跳跃表的结构分为多个节点,每个节点中包含分值、成员对象、层,分值代表每个节点存储的值,并且数据在节点中是顺序存储的,成员对象可以理解为每个节点保存的成员对象,层在跳跃表中是一个比较特色的部分,跳跃表之所以能实现快速查询就是依靠了层。

可以将节点理解为一层楼,成员对象存储在第一层,分值存储在第二层,第二层以上则就是层,每层都可以跟前面的节点进行连接。
如图所示,节点1指向了节点4的第九层,这样如果要访问6这个数据,那么可以直接从节点1指向节点4这个这个路径查找节点4中的数据6,这样走过的路径只有一次。
在这里插入图片描述

5.整数

Redis中存储整数时则使用了整数型的数据结构,Redis中的整数会区分16位,32位以及64位的数据,因此为了解决数据变换的问题,Redis中则使用了升级方法来实现数据的存储,如果一个键值中存储的数据是16位的数据,当对这个数据更改到32位时,那么16位的就不足以存储下这些数据,就需要将16位的键值升级,升级到32位进行存储更改的数,同时32位存储64位的数据也会执行升级,并且这个升级过程是不可逆的,这样也保证了以后数据跟更改之后不需要再进行升级。

二、持久化机制

1.RDB

RDB模式的持久化分为两个实现方式,SAVE和BGSAVE,SAVE实现比较粗暴,当执行SAVE命令进行持久化时,Redis会通过主线程来进行持久化操作,因此当进行持久化操作时其它任务就必须停止,必须等待持久化操作结束,其它任务才会进行,这种持久化方式会影响Redis任务的执行,一般不推荐使用。另一种是BGSAVE,BGSAVE比较温和,这种方式并不会占用Redis主线程,而是在主线程的基础上fork一个子线程来进行持久化操作,所以并不影响主线程处理Redis任务,只是会消耗一些性能。一般都使用BGSAVE来进行RDB的持久化。

另一方面,RDB持久化的是数据,也就是所谓的数据库状态,当我们向Redis数据库执行对数据的操作时,RDB持久化会将这些数据持久化到磁盘中,但是这种方式的持久化会丢失一部分数据,因为RDB持久化是从一个时间点开始持久化,某个时间点执行了RDB持久化,如果之后发生了宕机,那么该时间点之后的数据将会丢失。

最后对于RDB文件的载入,在Redis启动时会自动检测是否存在RDB文件,如果存在则对RDB文件进行载入,恢复RDB文件中的数据,但是Redis中如果开启了AOF持久化,那么Redis会优先载入AOF持久化文件。

2.AOF

为了弥补RDB持久化会丢失某个时间点数据的问题,Redis也存在另一个持久化方式AOF,AOF持久化与RDB持久化不同的地方在于持久化数据的不同,RDB持久化的是数据,而AOF持久化的是写命令,当对Redis执行写命令时,命令执行成功,如果开启了AOF持久化,那么就会将这些写命令存储到AOF缓冲区中。

AOF持久化的开启在Redis的配置文件中,通过设置appendonly参数来开启AOF持久化。

同时AOF持久化还具有三个模式,always、everysec、no,
always的意思是执行一个写命令就会将aof缓冲区中的数据写入到AOF文件中,并且同步AOF文件,
everysec的意思是执行一个写命令就将aof缓冲区中的数据写入到AOF文件中,并且每隔一秒就同步依次AOF文件,
no的意思是执行一个写命令就会将aof缓冲区中的数据写入到AOF文件中,但是不进行AOF文件的同步,同步AOF文件由操作系统决定。

从以上可以看到AOF的持久化会丢失更少的数据,因此当使用Redis时可以将两个持久化都开启,这样保证数据最少的丢失。

三、主从复制

Redis中有时需要同步其它服务器的数据,这时候就可以通过Redis中主从复制的功能来实现,对于Redis的主从复制有两个版本,2.8之前与2.8之后 ,先说2.8之前的。

2.8版本之前的主从复制是通过SYNC来实现,在Redis中SYNC的实现过程有两步,分别是同步和命令传播,同步主要实现的是将主数据库的持久化文件发送到从数据库,通过文件进行同步数据,命令传播实现的是在发送持久化文件之后,主数据库执行的各种命令会同步发送到从数据库中,这样保证了主从数据库的数据一致。

旧版:SYNC

1、同步

当从数据库执行SYNC发送给主数据库,主数据库收到SYNC命令,然后执行BGSAVE命令进行RDB持久化,然后将持久化的RDB文件发送给从数据库,从数据库接收文件并载入。主数据库在持久化之后执行的命令则会同步到缓冲区中。

2、命令传播

主从数据库完成同步操作之后,从数据库就有了主数据库中从BGSAVE持久化之前的所有数据了,那么主数据库之后执行产生的数据则会存放到缓冲区中,将缓冲区中的数据再同步给从数据库就实现了命令传播的功能,这样就保证了主数据库执行命令产生的数据也会及时发送给从数据库保证数据的一致性。
但是这种主从复制的方式会存在问题,就是当从数据库宕机之后,那么再进行主从复制时就需要再次发送RDB文件,然后再进行命令传播,但是从数据库中已经有了之前的数据,缺失的只是宕机之后的数据,这样执行全部的主从复制对于Redis来讲比较消耗性能,所以在2.8之后进行了优化。

新版:PSYNC

对于新版的主从复制的功能则是通过PSYNC来实现的,PSYNC有两种模式,一种是完整重同步,一种是部分重同步。当从数据库从来没有复制过主数据库的数据时,执行PSYNC命令则会使用完整重同步来进行同步数据,

1、完整重同步

完整重同步的过程跟旧版的SYNC的过程类似,也是先发送RDB文件,然后再进行命令传播。

2、部分重同步

而部分重同步则是为了处理断线重连的问题,对于部分重同步需要知道几个概念,复制偏移量,复制积压缓冲区、容器ID。

复制偏移量分为两个,一个是主数据库的偏移量指的是主数据库发送给从数据库数据偏移量,从数据库的偏移量指的是从数据库接收主数据库的数据偏移量,当从数据库的偏移量小于主数据库的偏移量时则证明需要进行主从复制。

复制积压缓冲区,当主数据库发送命令给从数据库时同时会将这些命令写入到复制积压缓冲区中,写入到缓冲区中的命令也同步了主数据库的偏移量,这个缓冲区也是为了进行同步最近的数据做的处理。

容器ID,容器ID指的是主数据库的ID,这个ID是为了防止当主数据库宕机,那么主数据库会变更,原来的主数据库就会失效,那么从数据库就无法从原来的主数据库进行复制,这时就需要容器ID来解决,当从数据库中保存的容器ID与要请求的主数据库的ID不一致时,则证明主数据库更改了,需要对新的主数据库执行完整重同步。

四、哨兵

概念:

Redis中哨兵的功能主要是管理Redis集群,当一个Redis集群中存在一个master和多个slave,当master出现断开连接的情况,就需要有一个管理员来进行维护整个集群的正常运行,而不是master服务器断开连接,那么整个集群就会崩溃的情况,这就是哨兵所要做的事情。

哨兵可以监控Redis集群中的所有Redis服务器,当出现master服务器断开连接的情况,那么哨兵就会发挥作用维护整个集群的运行。

实现:

哨兵监控到master断开连接,那么会将master服务器从监控列表中移除,并且从slave服务器中选举一个服务器作为新的master服务器,然后将旧的master服务器设置为slave服务器,这样当旧的master服务器重新恢复连接后会成功新master服务器的从服务器,而不会 导致旧master服务器重新连接需要跟新master抢master角色的问题,并且也保证了整个集群的稳定性。

在使用哨兵时一般是多个哨兵一同工作,并且哨兵之间进行一个投票机制,只有多个哨兵共同决定一个Redis服务器断开连接,这个服务器才会从哨兵的监视中去除,例如当有三个哨兵对一个集群进行监控,当master服务器断开连接,只有当两个哨兵都认可该服务器已下线,那么这个master服务器才会从集群中剔除掉。然后哨兵完成故障转移操作,选举新的master服务器,将旧的服务器转换为新master的从服务器。

哨兵监控服务器的状态是每十秒一次的频率进行发送监控命令。

五、集群

在使用Redis时一个Redis数据库并不能满足场景的使用,这时就需要Redis集群来解决追求高性能的场景。

每个Redis数据库都是一个集群,多个Redis数据库组成了一个整体的集群,当我们想要将一个数据库加入到集群中时,我们使用cluster meet命令来实现。

实现方法:CLUSTER MEET

过程:

Redis的集群开启是通过设定配置文件中的cluster-enable参数来实现的,当打开这个参数时,执行cluster meet命令就可以就可以将数据库加入到集群中。

集群的底层实现是基于一种clusterNode结构来实现的,集群中的每个节点都会存在一个clusterNode,该结构保存着节点的名称、节点IP、节点端口、当前纪元等。并且每个节点还会通过clusterState结构来保存该节点与集群中其它数据库的连接,clusterNode保存着集群的当前纪元、集群的状态、以及一个nodes数组,该数组的结构是clusterNode结构,所以nodes数组中就保存着其它数据库的clusterNode结构。

六、事务

事务在数据库中也接触过,对于事务的理解是多个操作顺序执行,要么全部执行成功,要么失败,只要有一个操作出现错误,原先执行的就要回滚并且后续不再执行。

而在Redis中也实现了事务的功能,Redis的事务是通过MULTI、EXEC、WATCH来实现的。

实现方法

MULTI命令是用来开启事务,当我们执行了MULTI命令,客户端就会开始事务,后续执行的命令就会被暂时缓存,等待事务的开启。

EXEC命令是执行事务,在执行MULTI命令后所执行的命令都会在EXEC命令执行之后进行顺序执行并返回结果。

WATCH命令则是监控事务中键的功能,通过WATCH命令可以监控我们操作的键,当我们操作的键被其它客户端修改,那么我们事务就不能执行成功。

过程

Redis事务的过程分为三步,分别为事务开始、命令入队、事务执行。

WATCH命令实现

当执行修改命令时,如果LPUSH、SET、SADD、DEL等命令时,如果操作的键是事务中的键,那么事务在执行时会执行失败并且返回一个空回复。
另外事务的一些特殊情况如下:当我们执行multi命令之后执行的各种命令出错,那么最后执行EXEC命令时,Redis在2.6.5之前会直接报错,2.6.5之后则不会执行这些错误命令。
在这里插入图片描述
如果执行MULTI命令之后执行的命令操作键的类型不对,那么最后通过EXEC执行事务时,事务会执行成功,但是错误的命令会报错。

在这里插入图片描述

七、缓存三大问题

缓存雪崩

缓存雪崩的发生场景是当缓存中的数据大量失效,那么大量的请求就会打到DB中,大量的请求导致DB崩溃造成缓存雪崩的情况。

对于缓存雪崩的情况,我们可以让缓存数据失效时间设置均匀一些,避免大量缓存时间一起失效。因为我们可以对缓存时间增加一个随机值,这样缓存时间失效的时间也是随机的,避免了缓存一起失效的问题。

既然缓存失效,那么还可以通过主从复制模式来避免,通过设定多个从数据库,当一个数据库的缓存失效了,可以让请求去其它数据库获取缓存,避免直接将请求打到DB中。

缓存穿透

缓存穿透的意思是大量的请求一个缓存和DB中都不存在的数据,导致这些大量的请求打到DB中,造成DB压力过大最终导致DB崩溃。比如在电商业务中请求一个id=-1的商品,在缓存以及DB中都不存在,当大量的这些请求打到DB就会导致DB崩溃。

对于这种情况我们可以通过以下方法来避免,第一,在接口中实现简单的过滤逻辑,可以在接口中增加判断条件将不符合的一些数据过滤避免请求到缓存中。第二,使用布隆过滤器,布隆过滤器是一个很长的二进制向量和一系列随机映射函数,可以用于检索一个元素是否在一个集合中,这样当一个请求携带id=-1到缓存中,通过布隆过滤器检索-1是否在缓存中,如果不存在则不会进入缓存中以及DB中,这样就保证了将请求拦截在上游不会打到缓存以及DB中。

布隆过滤器的实现主要是基于哈希函数来实现的,比如我们在hashMap中将数据存放的位置就是通过哈希函数实现的,但是单个的哈希函数会出现哈希碰撞的问题,而布隆过滤器为了解决这个问题就使用了多个哈希函数来实现,如果有一个哈希函数的值表明元素不在集合中,那么这个元素肯定就不在,如果所有哈希函数的值表明元素在集合中(当然也有可能误判),那么这个元素就在集合中。

所以布隆过滤器不需要存储元素本身就可以进行元素的判断,并且空间效率以及查询效率更好。

但是它也有一定的缺点就是误判的问题,它的判断并不是准确的,会存在一定的误差,随着元素的增加误差率也会增加,为了解决这个问题可以通过设定白名单。另外如果元素比较少可以直接使用散列表来实现过滤数据。

缓存击穿

缓存击穿的问题是由于当大量的请求访问一个缓存,而这个缓存在一瞬间失效了,那么就会导致这些请求一下子打到DB上,造成DB的崩溃,形成缓存击穿的情况。

缓存击穿可以想象电商抢购的情况,如果电商中一个商品进行抢购,大量的用户都在抢购这个商品进行下单,如果商品缓存时间突然失效,那么这个下单请求就会打到DB中,造成DB的崩溃。

所以对于这种问题可以对下单的操作进行加锁,只有用户的请求抢到了锁才能够进行下单,所以一瞬间也只有一个请求会进行下单,即使缓存失效也只有一个请求会打到DB中。

在Redis中可以通过设定分布式锁来实现,通过分布式锁限定同一时刻只有一个请求可以获取到该缓存数据。

八、分布式锁

针对以上缓存击穿的问题,Redis可以通过分布式锁来实现,保证同一时刻只有一个请求可以获取到该缓存数据。

Redis实现分布式锁可以通过setNX方法来实现,setNX方法是一个Redis添加缓存的方法,方法流程为,缓存的key和value请求到方法中,方法中调用SetParams的方法nx以及px方法,nx方法用于判断缓存中是否存在该key,px方法用于设置key的过期时间,当超过过期时间,那么这个key就会失效,当我们要删除这个key时通过lua脚本来实现,具体实现是通过lua脚本查询要删除的key跟缓存中的key是否相等以及value是否跟缓存中相等,如果相等则执行删除。

/**
     * 加锁
     * @param key
     * @param val
     * @return
     */
    public boolean setnx(String key, String val) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            if (jedis == null) {
                return false;
            }
            SetParams params = new SetParams();
            params.nx();
            params.px(1000*60);
            boolean b = jedis.set(key, val,params).
                    equalsIgnoreCase("ok");
            return b;
        } catch (Exception ex) {
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return false;
    }

    /**
     * 删除锁
     * @param key
     * @param val
     * @return
     */
    public int delnx(String key, String val) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            if (jedis == null) {
                return 0;
            }
            //if redis.call('get','orderkey')=='1111' then return redis.call('del','orderkey') else return 0 end
            StringBuilder sbScript = new StringBuilder();
            sbScript.append("if redis.call('get','").append(key).append("')").append("=='").append(val).append("'").
                    append(" then ").
                    append("    return redis.call('del','").append(key).append("')").
                    append(" else ").
                    append("    return 0").
                    append(" end");

            return Integer.valueOf(jedis.eval(sbScript.toString()).toString());
        } catch (Exception ex) {
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return 0;
    }

但是这样实现分布式锁还会存在一些问题,首先就是过期时间的问题,如果我们设定的过期时间为1s,但是一个用户在使用该key时1s之后还没有结束,那么这个key就会失效,而用户明明还没有使用完,所以这时候就需要考虑一个过期时间续约的问题。

对于如果续约过期时间在Redis中存在一个Watch Dog来解决这个问题,watch dog的功能就是实现监听,监听一个key和客户端的使用情况,如果超过了过期时间但是客户端依然在使用这个key,那么watch dog就会给这个客户端进行续约,直到它使用完。

除了Redis可以实现分布式锁,zookeeper也可以实现分布式锁,zookeeper中有持久节点以及临时节点,可以根据节点特性来实现。

首先创建一个持久节点perLock,所有需要获取锁的线程都会在该持久节点下创建一个临时顺序子节点,并且每次都是顺序最小的节点获取锁,当最小的节点处理完毕,断开本次连接,临时节点也就自动删除,然后让其它没有获取锁的节点获取锁。并且当一个客户端创建了一个临时节点,获取锁失败的时候就会找出序号最小的节点获取锁,然后在该节点上注册一个监听时间,当序号最小的节点断开连接释放锁之后,那么后面的节点再去获取锁。


总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值