redis内核(每周分享一个技术)

1.单线程为什么那么快

①redis是一个内存型的k-v形式的数据库,所有的数据都是存储在内存上中,读取,写入速度快,阈值是10毫秒

②redis的io是异步的,由redis server 分给epoll去处理(比如过期,删除),尽量避免在ridis中存入大key,超过500k就算是大key,一般读取大key

③redis基本没有日志,唯一写入是aof,类似binlog,去掉不必要的日志

2.Key-Value

①key的内部编码:在内存中已那种编码去存储:

String中是(Raw, init,Embstr),Raw也被称为原始string,如果数据用Raw格式存储,他就是原始的,磁盘有一种模式就是Raw模式,也被称为裸盘模式,在这个模式下,数据以原始的形式存储,你可以直接读取它。

INT格式,意味着它是一个整型数字。INT一般是整数,这个字符串是一个整形字符串,长度不是特别长的一个整形字符串,整形字符串整数数字,你是一个整数数字,我就会以INT形式存储。(用一个八位编码的整数就是init,long double 就是embstr存储)

EMBSTR会以它存储,就是当数值,就当是一串整形。(只读编码)

3.Memcached和redis的区别

Memcached:只能存储KV、没有持久化机制、不支持主从复制、是多线程

redis:数据类型丰富、支持多种编程语言、功能丰富(持久化机制,内存淘汰机制,事务,发布订阅,pipline,lua),支持集群,分布式

4.Hash与String的主要区别

①把所有相关的值都聚集到一个Key中,节省内存空间

②只使用一个Key,减少Key冲突

③当需要批量获取值的时候,只需要使用一个命令,减少内存io/cpu的消耗

④hash不适合的场景:Field不能单独设置过期时间,需要考虑数据量分布的问题

5.二进制SDS安全

redis3.2以前

struct sdshdr{
     int len;//buf数组中已经使用的字节的数量,也就是SDS字符串长度
        int  free;//buf数组中未使用的字节的数量
        char buf[];//字节数组,字符串就保存在这里面
};

3.2以后

struct_attribute_((_packed_))sdshdr8{
    uint8_t len;//当前字节数组的长度
    uint8_t alloc;//当前字节数组总共分配的内存大小
    uimt8_t flag;//当前字节数组的属性,用来标识到底是sdshdr8还是sdshdr16等
    char buf[]; //字符串真正的值
}

特点:

①不用担心内存溢出问题,如果需要会对SDS进行扩容

②获取字符串长度时间复杂度0(1),定义了属性

③通过空间预分配和惰性预分配,防止多次重分配

④判断是否结束的标志是len属性

6.RESP通信协议

序列化协议,容易实现,解析快,可读性强。把命令,长度和参数用/r/n连接起来

package redis;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
 
public class RespTest {
 
    private Socket socket;
    private OutputStream write;
    private InputStream read;
 
    public RespTest(String host, int port) throws IOException {
        socket = new Socket(host, port);
        write = socket.getOutputStream();
        read = socket.getInputStream();
    }
 
    /**
     * 实现了set方法
     * @param key
     * @param val
     * @throws IOException
     */
    public void set(String key, String val) throws IOException {
        StringBuffer sb = new StringBuffer();
        // 代表3个参数(set key value)
        sb.append("*3").append("
");
        // 第一个参数(set)的长度
        sb.append("$3").append("
");
        // 第一个参数的内容
        sb.append("SET").append("
");
 
        // 第二个参数key的长度(不定,动态获取)
        sb.append("$").append(key.getBytes().length).append("
");
        // 第二个参数key的内容
        sb.append(key).append("
");
        // 第三个参数value的长度(不定,动态获取)
        sb.append("$").append(val.getBytes().length).append("
");
        // 第三个参数value的内容
        sb.append(val).append("
");
 
        // 发送命令
        write.write(sb.toString().getBytes());
        byte[] bytes = new byte[1024];
        // 接收响应
        read.read(bytes);
        System.out.println("-------------set-------------");
        System.out.println(new String(bytes));
    }
}

7.PipLine

通过一个队列把所有的命令缓存起来,然后把多个命令在一次连接中发送给服务器,需要客户端和服务器都支持(类似sql的批量操作)

package com.redisdemo;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;

public class PipelineTest {
    public static void main(String[] args) {
       int num = 1000000;
        long start = System.currentTimeMillis();
       unPipeLine(num);
        long end = System.currentTimeMillis();
        System.out.println("没有使用Pipeline的执行时间: " + (end-start));

        start = System.currentTimeMillis();
        usePipeLine(num);
        end = System.currentTimeMillis();
        System.out.println("使用Pipeline的执行时间: " + (end-start));
    }

    /**
     * 没有使用PipeLine
     */
    public static void unPipeLine(int num){
        Jedis jedis = new Jedis("127.0.0.1",6379);
        for(int i = 0; i <= num;i++){
            jedis.set("unPipeLine:"+i,""+i);
        }
        jedis.disconnect();
    }

    /**
     * 使用PipeLine
     */
    public static void usePipeLine(int num){
        Jedis jedis = new Jedis("127.0.0.1",6379);
        Pipeline line = jedis.pipelined();
        for(int i = 0; i <= num;i++){
            line.set("usePipeLine:"+i,""+i);
        }
        line.sync();
        jedis.disconnect();
    }
}

8.jedis分布式锁

①.互斥性:只有一个客户端可以持有锁

②.不会产生死锁:即使持有锁的客户端崩溃,也能保证后续其他客户端可以获取锁

③.只有持有这把锁的客户端才能解锁

加锁

jedis.set(String key, String value, String nxxx, String expx, int time)

第一个为key,我们使用key来当锁,因为key是唯一的
第二个为value,我们传的是requestId,requestId是客户端的唯一标志。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经
存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定
第五个为time,与第四个参数相呼应,代表key的过期时间

解锁

public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId)
{
   //首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁
   String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return                    redis.call('del',KEYS[1]) else return 0 end";
   Object result = jedis.eval(script, Collections.singletonList(lockKey),         
   Collections.singletonList(requestId));
  if(RELEASE_SUCCESS.equals(result)) {
     return true;
   }
   return false;
}

9.过期策略(应该和业务方去确认)

redis中同时使用了惰性过期和定期过期两种策略,并不是实时地清除过期key

立即过期(主动淘汰)

每个设置过期时间的key都需要创建一个定时器,到过期时间就会立刻清楚。这个策略可以立即清楚过期的数据,对内存友好,但是会占用大量的cpu资源去处理过期数据,从而影响缓存和相应时间的吞吐量

惰性过期(被动淘汰)

只有当访问一个key时,才会判断改key是否过期,改策略可以最大化地节省cpu资源,对内存不好,极端情况下可能出现大量的过期key没有被访问,从而不会被清楚,占用大量的内存

定期过期

没过一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清楚其中已过期的key,通过定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得cpu和内存资源达到最优的平衡效果

10.淘汰策略

最大内存设置

①.参数设置(redis.conf的maxmemory)

②.动态修改(先get一下,config set maxmemory 2GB)

③.LRU:最近最少使用。判断最近被使用的时间,距离目前时间最远的数据优先被淘汰

④.最不常用,按照使用频率删除,4.0版本新增

⑤.random:随机删除

11.redis持久化机制

RDB

RDB的触发有三种机制,执行save命令:执行bgsave命令:在redis.config中配置自动化

redis默认持久化,RDB和AOF同时开启,默认加载AOF的配置文件,(开启了AOF,优先用AOF),不管是手动还是自动启动(第一次启动),会把内存中的数据写入到磁盘

自动触发配置

save 900 1#900秒内至少有一个key被修改

save 300 10#300秒内至少有10个key被修改

save 60 10000#60秒内至少有10000个key被修改

shutdown触发,保证服务器正常关闭

flushall,rdb文件是空的,没什么意义

手动触发

save:生成快照时会堵塞当前redis服务器,redis不能处理其他命令

bgsave:redis会在后台异步进行快照操作,可以同时响应客户端请求,redis进程会fork一个子进程,rdb持久化由子进程负责,完成自动结束

AOF

AOF日志存储的是Redis服务器指令序列,AOF只记录对内存进行修改的指令记录

AOF默认不开启,采用日志的形式来记录每个写操作,追加到文件中

AOF触发配置方式

appendfsync

always:每次收到写命令

everysec:每秒钟强制写入磁盘一次,是最有保证的完全持久化,速度最慢,不推荐

no:完全依赖os的写入,一般为30s一次,性能最好但是持久化最没有保证,不推荐

文件越来越大怎么办

使用命令bgrewriteaof重写,aof文件重写并不是对原文件进行重新整理,而是直接读取服务器中现有的键值对,然后用命令去代替之前这个键值对的多条命令,生成一个全新的文件代替原来的aof文件

重写触发机制

auto-aof-rewrite-percentage 100: aof文件增长比例,aof重写就是aof文件在一定大小之后,重新将整个内存写到aof文件中,反映最新的状态(想到于bgsave),这样就避免了,aof文件过大而实际内存数据小的问题(频繁修改数据问题)

auto-aof-rewrite-min-size 64mb:aof文件重写最小的文件大小,触发这个机制,刚开始aof文件必须要达到这个文件大小才触发,后面每次重写就不会根据这个变量,这个变量只有初始化启动redis有效,reids恢复时,lastSize等于初始aof文件大小

AOF重写过程中的命令

会放到一个aof重写缓存中,等子进程重写完后再把缓存中的命令追加到aof文件中。

Redis4.0混合持久化

就是新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据

在redis重启的时候,加载aof文件进行恢复数据:先加载rdb内容再加载剩余的aof

12.热点数据发现

redis的缓存数据淘汰机制,能够留下那些热点的key,不管是lru还是lfu。

客户端jedis的Connection类的sendCommand()里面,用一个HashMap进行key的统计

代理层比如TwemProxy或者Codis,(twemProxy)代理工具,其主要功能是对多个 Redis 实例提供透明化的访问支持。通过 TwemProxy 可以实现数据分片、负载均衡以及高可用性的需求

服务端Redis有一个monitor的命令,可以监控到所有的Redis执行的命令。

机器层面通过对tcp协议进行抓包,比如elk的packetbeat插件

13.缓存雪崩

缓存雪崩就是Redis的大量热点数据同时过期,因为设置了相同的过期时间,同时这个时候Redis请求的并发量又很大,就会导致所有的请求落入到数据库中

解决方案

1.加互斥锁或者使用队列,针对同一个key只允许一个线程到数据库查询

2.缓存定时预先更新,避免同时失效

3.加随机数,使key在不同的时间过期

4.缓存永不过期

5.redis高可用

14.缓存穿透

按照key去缓存查询,如果不存在对应的value,就应该去查找数据库,key对应的value是一定不存在的,并且对该key并发量很大,就对数据库造成很大的压力,就是缓存穿透

解决方案

1.缓存空数据

2.缓存特殊字符串,比如&&

3.布隆过滤器

15.缓存击穿

key对应的数据存在,单子redis中过期,此时有大量的并发请求过来,但是请求发现缓存过期一般都会从数据库加载数据返回缓存,这个时候大量的请求可能会瞬间压垮数据库。

解决方案

1.互斥锁加锁排队

2.缓存永不过期

16.布隆过滤器(存在一定的误判率)

从容器角度来说:

1.如果布隆过滤器判断元素在集合中存在,不一定存在

2.如果布隆过滤器判断不存在,一定不存在

从元素角度来说:

1.如果元素实际存在,布隆过滤器一定判断存在

2.如果元素实际不存在,布隆过滤器可能判断存在

17.reids内存满了怎么办

这个和redis的回收策略有关

1.Redis的默认回收策略是noenviction,当内存用完之后,写数据会报错

2.Redis的其他内存回收策略含义:

volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中,淘汰最近最少使用的数据

volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中,淘汰最早会过期的数据

volatile-random:从已设置过期时间的数据集(server.db[i].expires)中,随机淘汰数据

allkeys-lru:从数据集(server.db[i].dict)中,淘汰最近最少使用的数据

allkeys-random:从数据集(server.db[i].dict)中,随机淘汰数据

18.redis性能优化

1.尽量使用短key

2.避免使用keys* 会堵塞

3.在存在到Redis之前先把你的数据压缩一下

4.设置key有效期

5.选择回收策略(maxmemory-policy)

6.限制redis的内存大小避免发生oom

7.当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能

8.想要一次添加多条数据的时候可以使用管道

9.尽可能地使用hash存储

10.使用bit位级别操作和byte级别操作来减少不必要的内存使用

19.缓存的类型

本地缓存(进程缓存Ehcache)、分布式缓存、多级缓存

20.Memcache的特点

1.Mc处理请求时使用多线程异步io的方式,可以合理利用cpu多核的优势,性能非常优秀

2.MC功能简单,使用内存存储数据,只支持K-V结构,不提供持久化和主从同步功能

3.MC对缓存的数据可以设置失效期,过期后的数据会被清除

4.失效的策略采用延迟失效,就是当再次使用数据时检查是否失效

5.当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期key进行清理,还会按LRU策略对数据进行剔除

6.限制:key不能超过250个字节、value不能超过1M字节

21.集群模式下导致重复加锁怎么办(Redisson)

Redission会自动选择同一个master加锁

22.业务没有执行完,分布式锁到期了怎么办

看门狗

23.布隆过滤器为什么没有删除功能

存在hash碰撞

24.redis缓存过期通知

1.事件通过Redis的订阅和发布来进行分发,需要开启redis的事件监听与发布

2.修改redis.conf文件,打开notify-keyspace-eventsEx的注释,开启过期通知功能

3.重启redis,即可测试失效事件的触发,监听获取的值为key

25.什么时io多路复用

Io指的是网络I/O,多路指的是多个TCP连接,复用指的是复用一个或多个线程。

基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。

客户端在操作的时候,会产生具有不同事件类型的socket,在服务端,I/O多路复用程序会把消息放入队列中,然后通过文件事件派发器转发到不同的事件处理器中。

多路复用有很多的实现,以select为例,当用户进程调用了多路复用器,进程会被堵塞,内核会监视多路复用器负责的所有socket,当任何一个socket的数据准备好了,多路复用器就会返回。这时候用户进程再调用read操作,把数据从内核缓冲区拷贝到用户空间。

IO多路复用的特点是通过一种机制让一个进程能同时等待多个文件描述符,而这些文件描述符其中的任何一个进入读就绪状态,select()函数就可以返回。

26.分布式的含义

高性能、高可用、扩展性需要依赖两种关键的技术,一种是分片,一种是冗余。分片的意思是把所有的数据拆分到多个节点分撒存储。冗余的意思是每个节点都有一个或者多个副本。那么,redis必须要提供数据分片和主从复制的功能。副本有不同的角色,如果主节点发生故障,则把某个从节点改成主节点,访问新的主节点,实现高可用。

27.主从复制原理

主从复制分为两种

全量复制、增量复制

连接阶段

1.salve节点启动时,会在自己本地保存master节点的信息(ip、port等)

2.slave节点内部有个定时任务,每隔一秒钟检查是否有新的master节点连接和复制。

3.如果发现有master节点,就跟master节点建立连接。如果连接成功,从节点就为连接分配一个专门处理复制工作的文件事件处理器负责后续的复制工作。为了让主节点感知到slave节点的存活,slave节点定时回个主节点发送ping请求。

数据同步阶段

如果新加入的master节点,那就需要全量复制。master通过bgsave命令在本地生成一份RDB快照,将RDB快照发送给salve

节点。slave如果有数据,首先清除自己的旧数据,然后用RDB文件加载数据。开始生成RDB文件时,master会把所有新的写

命令缓存在内存中,在slave节点保存RDB文件后,再把新的写命令复制给slave节点。

命令传播阶段

master节点持续把写命令异步复制给slave节点

增量复制

slave通过master_repl_offset记录的偏移量

盘复制redis6.0

master节点的RDB文件不保存到磁盘而是直接通过网络发送给从节点。适用于master节点磁盘性能不好但是网络好。

28.哨兵

Sentinel原理

-redis的高可用是通过哨兵保证的。它的思路是通过运行监控服务器来保证服务可用性。一般是奇数节点个数防止脑裂。

-哨兵集群监控所有的redis节点,哨兵之间也相互监控没有主从之分地位平等。

-哨兵通过发布订阅功能监控所有的redis节点、哨兵之间是否在线。哨兵订阅(_sentinel_:hello)

-最大作用监控服务状态、切换主从

服务下线

主观下线:哨兵默认以每秒钟1次的频率向redis服务节点发送ping命令。如果在指定时间(默认30秒)内没有收到有效回复,哨兵会被标记为主观下线。

客观下线:第一次发现master下线的哨兵继续询问其他的哨兵节点,如果多数哨兵都认为master节点下线,master才会被 标记为客观下线。

故障转移

-哨兵集群选取reader,由reader完成故障转移,通过raft算法实现哨兵选举

-数据一致需要两个步骤:领导选举,数据复制。

-raft算法是共识算法。raft核心思想:先到先得,少数复从多数

影响选举结果因素:断开连接时长/优先级id/复制数量/进程id

29.redis分布式

-数据分片:客户端、代理层、服务端

-客户端分片:ShardedJedis使用的是一致性哈希算法,如果数据分布不均使用虚拟节点

-代理层分片:Twemproxy、Codis

Redis Cluster(去中心化):

-Redis的数据分布即没有用哈希取摸,也没有用一致性算法,而是使用虚拟槽来实现的。

-redis创建16384个槽,每个节点负责一定区间Slot。

-对象分布到Redis节点上时,对Key用CRC16算法计算在%16384,得到一个slot的值,数据落到负责这个slot的redis节点上。

-Redis的每个master节点都会维护自己负责的slot,用一个bit序列实现。

-key和槽位的关系是不会变的,会变的是槽位和节点的关系。

-相关的数据落到同一个节点上可以使用{hash tag},{}这里面的字符串是相同的。

-客户端重定向:先会返回MOVED 13724 127.0.0.1:7293,再更换端口:redis-cli -p 7293。

数据迁移:因为key和slot的关系是永远不会变的,当新增节点的时候,需要把原有的slot分配的新的节点负责,并且把相关的数据迁移过来。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容