Redis总结

Posted by lizhao on 07-26,2019

Redis

@[toc]

总览

redis的线程模型

基础

和memcache的区别

  1. 数据结构
  2. 内存使用率,key-value的话memcache更好
  3. 效率,单个value的大小100k以上redis更好
  4. 集群部署,redis有原生支持

为什么单线程能有很高的效率

具体原因

  1. 单线程模型,避免了上下文切换
  2. IO多路复用机制
  3. 纯内存操作

连接过程

文件事件处理器(网络事件处理器、file event handler),这个是单线程的采用IO多路复用机制监听多个socket。

socket进来之后,如果有事件(比如说连接),IO多路复用程序就会将这个socket(这时候连接已经和connect事件绑定) 推到消息队列中。

文件事件分派器从队列中取出socket,检查事件,根据不同的事件分给不同的处理器。

包括多个socket,io多路复用程序,消息队列,文件事件分派器,事件处理器(命令请求处理器、命令回复处理器、连接应答处理器,等等) redis的线程模型

一次连接流程

  1. 服务端打开socket监听
  2. 客户端和服务端连接socket,这时候产生一个connect事件,后台表示为AE_READABLE
  3. io多路复用程序将这个socket推到消息队列里面
  4. 分派器判断是 连接应答处理器,进行处理,连接成功
  5. 这时候将socket和ae_writeable绑定,
  6. io多路复用程序看到这个又有事件了,就又推到消息队列
  7. 分派器判断是 命令回复处理器,就返回数据,然后和这个事件取消关联

多个socket,io多路复用程序,消息队列,文件事件分派器,事件处理器(命令请求处理器、命令回复处理器、连接应答处理器,等等)

哪些类型

string

  1. 字符串
  2. set key val,get key
  3. 简单的key-value存储,部门组织树,用户数据

list

  1. 数组
  2. lpush key val1 val2,lpop key,lrange key 0 10
  3. 存粉丝、评论、lrange可以分页,消息队列

hash

  1. 键值对
  2. hmset key key1 val1 key2 val2,hget key key1
  3. 用户信息-鉴权码-私钥

set

  1. 不重复无序列表
  2. sadd key val1 val2,smembers key
  3. 部门关系缓存

sort set

  1. 有序数组/带权重值列表
  2. zadd key score1 val1 score2 val2,zrangebyscore key
  3. 排行榜

从海量数据中查找某个key前缀

keys

keys pattern,

一次性返回全部满足条件数据,会阻塞redis

scan

scan cursor pattern count,

按pattern条件从下标cursor开始找count个数据,不一定会是count,大致相等。返回结果包括下一个游标位置和列表

持久化

持久化的意义

  1. 故障恢复
  2. 云备份到一个存储上

rdb

  1. 内存快照的形式
  2. RDB方式,sava 600 10,600秒内有10次写操作,则触发。
  3. 将数据快照保存,有可能丢失数据。
  4. 优点:适合做冷备份、性能(不需要每时每刻),恢复快
  5. 缺点:丢数据

aof

  1. 把所有操作指令保存下来,存到一个文件中
  2. 内存和文件中有一层os-cache,每隔1s会调用f-sync
  3. 一次只会写一个aof文件
  4. aof文件不可能无限增大,BG-REWRITE-AOF。会根据当前快照,进行重写aof文件
  5. 优点:数据丢少(1s),append-only模式写磁盘-速度快,记录是人可读的
  6. 缺点:占用磁盘大,qps写会降低,脆弱点,数据恢复比较慢

序列化方式

JdkSerializationRedisSerializer

使用JDK提供的序列化功能。 优点是反序列化时不需要提供类型信息(class),但缺点是需要实现Serializable接口,还有序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。

GenericJackson2JsonRedisSerializer

StringRedisSerializer

不能序列化Bean,只能序列化字符串类型的数据, 如果value都是字符串类型,可以用该方式序列化

GenericFastJsonRedisSerializer

数据过期/淘汰

  1. 这个是缓存,有容量限制
  2. 过期之后,还是占用内存

过期策略

设置了过期时间的key什么时候删除?定期删除和惰性删除,

这2个结合起来还是有可能漏掉一些key,这时候就需要内存淘汰机制登场

定期删除

每隔100ms随机抽去一些设置了超时时间的key,检查是否过期

过期则删除

这个会导致有可能一些key已经过期,但是没有删掉

惰性删除

查询某个key的时候,惰性检查,是否过期

如果过期则返回空

内存淘汰机制

redis内存占用过多的时候,会进行内存淘汰

具体策略
  1. noeviction,报错
  2. allkeys-lru,所有key走lru算法
  3. allkeys-random,所有key走随机删除
  4. volatile-lru,设置过期时间走lru算法
  5. volatile-random,设置过期时间的key走随机删除
  6. volatile-ttl,设置过期时间的key走"按过期时间最短"的算法

LRU代码实现

链表+hashmap

add、remove、refresh用来操作链表

get、put用来提供api

lru

package com.lizhaoblog.code.io.redis;

import java.util.HashMap;

class Node {
    public Node(String key, String value) {
        this.key = key;
        this.value = value;
    }

    public Node pre;
    public Node next;
    public String key;
    public String value;
}

public class LRUCache {
    private Node head;
    private Node end;
    //缓存上限
    private int limit;
    private HashMap<String,Node> map;

    public LRUCache(int limit) {
        this.limit = limit;
        map = new HashMap();
    }

    public String get(String key) {
        Node node = map.get(key);
        if (node == null) {
            return null;
        }
        //调整node到尾部
        refreshNode(node);
        return node.value;
    }

    public void put(String key, String value) {
        Node node = map.get(key);
        if (node == null) {
            //key不存在直接插入
            while (map.size() >= limit) {
                //去除链表内的节点
                String oldKey = removeNode(head);
                //去除map中的缓存
                map.remove(oldKey);
            }
            node = new Node(key, value);
            //链表中加入节点
            addNode(node);
            //map中加入节点
            map.put(key, node);
        } else {
            //更新节点并调整到尾部
            node.value = value;
            refreshNode(node);
        }
    }

    private void refreshNode(Node node) {
        //如果访问的是尾节点,无须移动节点
        if (node == end) {
            return;
        }
        //把节点移动到尾部,相当于做一次删除插入操作
        removeNode(node);
        addNode(node);
    }

    private String removeNode(Node node) {
        //尾节点
        if (node == end) {
            end = end.pre;
        } else if (node == head) {
            //头结点
            head = head.next;
        } else {
            //中间节点
            node.pre.next = node.next;
            node.next.pre = node.pre;
        }
        return node.key;
    }

    private void addNode(Node node) {
        if (end != null) {
            end.next = node;
            node.pre = end;
            node.next = null;
        }
        end = node;
        if (head == null) {
            head = node;
        }
    }
}

应用

分布式锁

  1. 互斥
  2. 死锁
  3. 容错

解决方案

  1. 正常使用redis的nx数据,下面的语句,key使用对应的前缀+主键,value使用一个随机值UUID,超时时间设置为30秒。
set key value nx 30

解锁:使用lua脚本,获取key值,判断和原先存起来的随机值相同吗,相同就删除,不相同表示redis中的这条数据不是你插入的,就不能删

  1. 基于Redis的RedLock,需要在集群环境中进行,同样的操作,但是需要保证同时在集群一半以上的机器上加上锁,才算是加锁成功。解锁同样
  2. 基于Zookeeper,设置一个临时节点(在自己的机器挂掉之后,会自动删除这个节点),设置成功就加锁,设置不成功这设置一个监听器,监听这个节点删除的事件
设置不成功这设置一个监听器,监听这个节点删除的事件

设置不成功,加一个CountDownLatch阻塞线程
监听器中释放CountDownLatch

结合实际

  1. 在公司使用的是分布式锁,客户分类管理
  2. 产品自动发布操作
  3. 产品预约

异步队列

集群

生产问题

缓存雪崩

现象

  1. 缓存挂掉了,请求直接打到数据库上,导致数据库也直接挂掉了

解决方案

  1. 事前:redis高可用,主从架构
  2. 事中:本地缓存、hystrix。到数据库的请求不能超过某个阈值
  3. 事后:需要做持久化,快速启动redis恢复 01_缓存雪崩现象 02_如何解决缓存雪崩

如何应对缓存穿透

现象

查询某个数据库肯定没有的数据,比如id是负数的

因为数据库没有,肯定不会保存到缓存上面,所以都会走库,这时候数据库压力就特别大

解决方案

  1. 不存在的值也存到缓存中
  2. 布隆过滤器,bitmap

双写一致性

cache aside pattern(普通模式)

  1. 读的时候,缓存有数据就返回,没有走数据库--插入缓存--返回
  2. 更新的时候,删除缓存,更新数据库
  3. 更新先删的原因:某些value是多个表联合查询出来的,比如通过一些计算算出来
  4. 更新先删的原因2:频繁修改,读比较少,lazy加载的思想

读写并发情况下的情景

  1. 数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改
  2. 读取更新并发情况下
解决
  1. 串行化
  2. 在服务中搞一些消息队列,根据主键hash 找到处理的队列 复杂的数据库+缓存双写一致保障方案
问题
  1. 读请求长时阻塞
  2. 读请求并发量过高
  3. 多服务实例部署的请求路由
  4. 热点商品的路由问题,导致请求的倾斜

并发竞争

现象

  1. 多服务并发写
  2. 多服务并发读

解决

分布式锁+时间戳

分布式锁确保同一时间只有一个实例在操作

时间戳用来判断是否需要插入,旧的话就不插入了

结合实际

  1. 主从模式,开启rdb和aof
  2. 数据大概小几十M

问题

replication架构

承载高并发

  1. 通过读写分离,读往master节点,读往slave从节点
  2. 水平扩容
  3. replication架构--主从架构

一些基本概念

offset

master和salve中都会存有一个offset,可以用来了解主从之间的数据差异。

backlog

默认1mb,在做复制的时候,会保存一份

用来做全量复制意外中断的时候 做增量复制用的

master run id

master启动的时候就会生成一个id(相当于一个uuid),表示的是这个节点的唯一键,如果主节点挂掉又重启了,从节点就会执行全量复制

psync

从节点发送这个命令去主节点拿数据,psync runid offset

根据这个返回增量复制

heartbeat

  1. 心跳包
  2. 主节点10秒一次发给从节点
  3. 冲击诶单1s一次发给主节点

异步复制

给从节点的数据,都是在异步线程执行的

最基本的功能

  1. 写master,异步写到salve节点
  2. 一个master可以挂多个salve
  3. salve节点在复制新数据的时候,如果查询进来,还是返回旧数据
  4. 主节点一定要开启持久化

复制的核心原理、过程

  1. 从节点配置文件中保存主节点信息,加载到内存中
  2. 定时任务:每秒钟检测是否有master需要连接和复制,如果有的话就需要建立连接
  3. 发送ping,连接主节点
  4. 如果有账号密码,需要发过去
  5. 全量复制,将所有的数据发过去
  6. 后续所有写操作,也都转发过去 复制的完整的基本流程

正常情况下(已经连接成功)、增量复制

  1. 2边数据一致,这时候进来一个写请求
  2. redis主线程写到 master,返回
  3. 异步线程和salve通信,将这条记录写过去
增量复制详细流程
  1. 全量复制过程中,主从连接断掉,当重新连接的时候,会触发增量复制
  2. master会将backlog中获取部分丢失的数据,发送给salve
  3. master根据slave发送的psync中的offset从backlog中获取数据

全量复制,异常情况(太久没连上、第一次连接)

  1. 主节点生成rdb文件
  2. 从节点将rdb文件取到,持久化到本地
  3. 从节点使用该rdb文件reload
  4. 如果在2.3步骤的过程中主节点有数据变更,会将这些数据用上一个方法复制
全量复制详细流程
  1. 主节点生成bgsave,生成一份rdb文件
  2. 主节点发送rdb文件给从节点,默认时间是60s判断是否正常发送成功
  3. 文件过大的话,这个有可能60s发不完
  4. 在发送的这段时间,主节点会将所有的写命令存到内存中,等从节点保存完rdb之后,将这些命令复制给从节点
  5. 有个参数,表示如果 环节4的缓存 超过64m,就判定复制失败
  6. 从节点接收到rdb之后,清空旧数据,重新加载rdb到内存中,期间使用旧数据对外服务
  7. 如果从节点开启aof的rewrite,则会立即重写aof

redis提供的功能

主从复制的断点续传

  1. 主节点中维护一个backlog和一个offset
  2. 从节点断掉重连,就从offset继续复制
  3. 如果offset不匹配,则触发全量复制 full resynchronization

无磁盘复制

全量复制的时候,主节点不生成rdb文件,而是直接生成到内存中,复制过去

过期key处理

只会是master处理

哨兵 sentinal

一般是三节点哨兵,2节点哨兵无法进行选举

功能

  1. 集群监控
  2. 消息通知,实例有故障,发消息通知管理员
  3. 故障转移,主备切换
  4. 配置中心,主备切换之后,通知给从节点

原理

sdown和odown的转换机制
  1. sdown,是主观宕机,哨兵ping不通(设置中的一个毫秒时间数)主机主节点就认定失败
  2. odown,客观宕机,quorum个哨兵觉得master宕机,就是觉得宕机了
哨兵集群的自动发现机制
  1. redis中的发布订阅
  2. 每个哨兵都订阅,sentinal——channel
  3. 每2秒钟,哨兵都往这个channel中写入:自己的节点信息及监控的信息
slave配置的自动纠正
  1. 纠正slave的配置信息
  2. master如果改变,则需要改变slave上面的配置信息
  3. 每个配置信息都有一个版本号,如果版本号比当前的新,则更新
quorum 和 majority
  1. quorum用来判定某个master是否宕机
  2. majority用来授权主备切换
选举算法
  1. quorum个哨兵判定master宕机,majority同意主备切换
  2. 按照以下顺序进行选举
    1. 设定了优先级的
    2. offset数据
    3. runid

2种数据丢失现象及解决方案

异步复制导致数据丢失

master节点在主线程写完数据,异步复制过程中挂掉了,这部分数据丢失

脑裂导致数据丢失

某个master和 salve-哨兵 网络连接错误,导致slave选举成 master,这时候集群中有2个master。

client 还往master中写数据,等到 master和slave连接恢复的时候,master转换成slave,会从新master同步数据,这时候,原来的master数据就丢失了

解决方案
  1. 参数
min-slaves-to-write 1
min-slaves-max-lag 10
  1. 表示至少有一个slave,数据复制和同步的延迟不能超过10s
  2. 如果主节点和从节点之间的通信或者数据偏移量超过10s,则判定master宕机

集群

使用redis自带的cluster架构可以支撑高并发高可用海量数据

数据分布算法

hash余数算法

一致性hash

  1. 圆环,以手表为例。假设为12个格子
  2. 按照机器的id进行hash,A找到一个位置(比如3点),B找到一个位置(比如7点),C找到一个位置10点
  3. 那么后面数据在3-7范围里面,就放到A节点上,7-10的就放到B节点上面。
  4. 如果其中一个挂掉了,就会将这些数据分到前一个节点上
  5. 优化:每一个节点又同时分成多个,这样避免挂掉之后,数据大量进入。 比如A节点改成369、B节点147

hash slot

  1. 可以看成一个hashmap,有16384个桶
  2. 每个节点负责 16384/节点总数 个桶
  3. 数据进来的时候,会通过 CRC16算法对16384取余,分配到不同的节点去

维护集群元数据

CAP

一致性、可用性、分区容错

集中式存储(基于zookeeper)

  1. 将所有的元数据存在走zookeeper中,有问题能直接反馈
  2. 缺点:集中、元数据存储压力

gossip协议

  1. 互相传播、将更新陆续传到各个节点
  2. 所有节点都持有一份元数据,数据变更,会慢慢传播到各个节点
  3. 一次最多发给5个节点。5个没有通信的节点
  4. 缺点:延时、滞后
通信端口

原来的端口加上10000

几个指令
ping
  1. 发送自己维护的元数据给其他节点
  2. 一次最多选择5个节点(最久没有通信的)
  3. 如果某个节点太久没有通信,则也会立即发给他
pong

收到ping指令后返回的数据,包含自身的状态信息

fail

发现某个节点挂掉的话,就发送这个指令到其他节点