Redis进阶

数据淘汰策略

redis数据存储在内存中,对于内存的使用的大小是配置的固定的值(maxmemory <bytes>),当存储的数据达到阈值的时候,需要对一些数据进行淘汰(即从内存中剔除),保证内存可用。默认是无策略(noeviction)。

  • noeviction

maxmemory-policy noeviction

无策略,不会进行删除操作,如果内存溢出则报错返回。

  • volatile-lru(Least Recently Used)

设置过期时间的数据中,剔除最不常用的数据。

  • allkeys-lru

从全部数据中(包括未设置过期时间的数据),根据时间删除最不常用的数据。

  • volatile-lfu(Least Frequently Used)

从配置了过期的时间的键中淘汰使用频率最少的数据。

  • allkeys-lfu

从全部数据中(包括未设置过期时间的数据),删除使用频率最少的数据。

  • volatile-random

设置了过期时间的数据中随机删除。

  • volatile-ttl

查询全部设置过期时间的数据排序,将马上要过期的数据进行删除操作。

类型扩展

pipeline

需要命令之间没有关联关系,快速提高操作性能(减少网络消耗)。

但pipeline的命令数量需要控制。

1. pipeline不是原子性操作,mset是原子性操作;

2. 有些数据结构没有一次发送多个执行命令的功能,如zset、hash不能一次添加多个key;

示例

    @Test
    public void test_basic_batch(){
        Long start = System.currentTimeMillis();

        for (int i = 0; i < 1000; i++) {
            jedis.set("basic:key" + i, "value");
        }

        Long end = System.currentTimeMillis();
        // 1000 -> 49652ms
        System.out.println("耗费时间:" + (end - start));
    }

    @Test
    public void test_pipeline(){
        Long start = System.currentTimeMillis();

        Pipeline pipeline = jedis.pipelined();
        for (int i = 0; i < 1000; i++) {
            pipeline.set("pipeline:key" + i, "value");
        }
        pipeline.sync();

        Long end = System.currentTimeMillis();
        // 1000 -> 131ms
        System.out.println("耗费时间:" + (end - start));
    }

发布订阅

角色:publisher(发布者)、subscriber(订阅者)、channel(频道)。

 示例

    @Test
    public void test_publisher(){
        for (int i = 0; i < 100; i++) {
            jedis.publish("1001", i + "");
        }
    }

    @Test
    public void test_subscriber(){
        JedisPubSub jedisPubSub = new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
                super.onMessage(channel, message);
                System.out.println("channel: " + channel + "; message: " + message);
            }
        };
        jedis.subscribe(jedisPubSub, "1001");
    }

Bitmap

即Bitset,是一串连续的二进制数字,本质是字符串,每一位所在的位置为偏移(offset),在bitmap上可执行AND、OR、XOR以及其他位操作,可以表示2^32个位,大概46亿位,redis单个key最大是512M,所以使用bitmap存储40多亿的数也就占500多M而已,非常节约空间。

使用场景:

实现记录用户哪天进行了登录,每天只记录是否登录过,重复状态算已登录,不需要记录操作行为等详细数据。

示例:

    @Test
    public void init_data(){
        jedis.setbit("user:login:20210101", 1, "1");
        jedis.setbit("user:login:20210101", 3, "1");
        jedis.setbit("user:login:20210101", 5, "1");
        jedis.setbit("user:login:20210101", 8, "1");

        jedis.setbit("user:login:20210102", 11, "1");
        jedis.setbit("user:login:20210102", 3, "1");
        jedis.setbit("user:login:20210102", 5, "1");
        jedis.setbit("user:login:20210102", 18, "1");

        jedis.setbit("user:login:20210103", 1, "1");
        jedis.setbit("user:login:20210103", 3, "1");
        jedis.setbit("user:login:20210103", 5, "1");
        jedis.setbit("user:login:20210103", 20, "1");
    }

    @Test
    public void count(){
        // 20210101 有多少用户登录
        System.out.println(jedis.bitcount("user:login:20210101")); // 4
        // 最近三天 20210101 20210102 20210103 有多少用户登录
        jedis.bitop(BitOP.OR, "user:login:31", "user:login:20210101", "user:login:20210102", "user:login:20210103");
        System.out.println(jedis.bitcount("user:login:31")); // 7
        // 统计连续登录三天的用户
        jedis.bitop(BitOP.AND, "user:login:32", "user:login:20210101", "user:login:20210102", "user:login:20210103");
        System.out.println(jedis.bitcount("user:login:32")); // 2

        System.out.println(jedis.bitpos("user:login:32", true));
    }

查看占用大小

>debug object set:data
"Value at:0x7f3ba8296f90 refcount:1 encoding:hashtable serializedlength:29874 lru:2664839 lru_seconds_idle:12"
>debug object bitmap:data
"Value at:0x7f3ba8296fb0 refcount:1 encoding:raw serializedlength:25 lru:2664857 lru_seconds_idle:6"

Hyperloglog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog的优点是在输入元素的数量或体积非常大时,计算基数所需的空间总是固定的、并且是很小的;在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64(约42亿)个不同元素的基数,这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

特点:

        具有一定的错误率(0.81%);

        无法获取单条数据;

        使用很小的内存;

        类型是字符串;

示例:

    @Test
    public void test_hyperloglog(){
        Pipeline pipeline = jedis.pipelined();
        for (int i = 0; i < 100000; i++) {
            pipeline.pfadd("loglog:data", i + "");
        }
        pipeline.sync();
        System.out.println(jedis.pfcount("loglog:data")); // 99565
    }

约12k 

>debug object loglog:data
"Value at:0x7f3ba8371030 refcount:1 encoding:raw serializedlength:10550 lru:2665502 lru_seconds_idle:21"

GEO

存储地理位置空间,类型是zset。

常用命令

将指定的地理位置空间(经度、维度、名称)保存
> genadd key longitude latitude member [longitude latitude member ...]

使用场景

地图的经纬度查询

https://lbs.amap.com/tools/picker

public class GEOPosition {
    // 经纬度等信息的封装
    /**
     * 经度
     */
    private Double longitude;

    /**
     * 纬度
     */
    private Double latitude;

    /**
     * 名称
     */
    private String name;
}

public class GeoTest {
    private Jedis jedis;
    /**
     * 计算附近10公里的酒店
     * 需要维护所有酒店信息(经纬度)
     */

    @Test
    public void init_geo(){
        GEOPosition geoPosition1 = new GEOPosition(121.477939,31.229639, "a");
        GEOPosition geoPosition2 = new GEOPosition(121.481672,31.23169, "b");
        GEOPosition geoPosition3 = new GEOPosition(121.481447,31.229846, "b");
        GEOPosition geoPosition4 = new GEOPosition(121.484623,31.232061, "d");

        jedis.geoadd("hotel", geoPosition1.getLongitude(), geoPosition1.getLatitude(), geoPosition1.getName());
        jedis.geoadd("hotel", geoPosition2.getLongitude(), geoPosition2.getLatitude(), geoPosition2.getName());
        jedis.geoadd("hotel", geoPosition3.getLongitude(), geoPosition3.getLatitude(), geoPosition3.getName());
        jedis.geoadd("hotel", geoPosition4.getLongitude(), geoPosition4.getLatitude(), geoPosition4.getName());
    }

    @Test
    public void test_geo(){
        // 统计x范围内的酒店
        List<GeoRadiusResponse> res = jedis.georadius("hotel", 121.474945,31.228777, 3, GeoUnit.KM);
        for (GeoRadiusResponse re : res) {
            System.out.println(new String(re.getMember())); // a b d
        }

        // 查看距离信息
        System.out.println(jedis.geodist("hotel", "a", "b", GeoUnit.KM)); // 334.4739
    }
}

持久化

redis所有的数据都是保存在内存当中,对数据的更新将异步或同步的保存到磁盘上。

作用

  • 保证数据的不丢失,重启服务redis内存当中的数据会清空;
  • 数据备份,可以完成数据的指定时间恢复;

持久化的方式

  • 快照(RDB)

将内存中的数据拷贝一份,以二进制格式存储RDB文件在磁盘中;重启服务后将该RDB文件装载到内存中。

主动触发方式

        1. save(同步):如果存在老的rdb文件,新的文件会替换老的文件;

        2. bgsave(异步):已使用异步处理,使用linux的fork函数;

        3. 自动配置(redis.conf中配置策略):会触发bgsave生成文件

# save {seconds} {keycount}
# 900秒内有一个key改变
save 900 1 
# 200秒内有10个key改变
save 300 10 
# 60秒内有10000个key改变
save 60 10000

自动触发机制

        1. 全量复制;

        2. debug reload;

        3. shutdown;

相关配置:

# 备份文件
dbfilename dump.rdb
# 备份目录
dir path/data
# 在出现错误时终止rdb备份
stop-writes-on-bgsave-error yes
# 是否进行压缩
rdbcompression yes
# 是否进行检查sum值校验
rdbchecksum yes

缺点:

耗时,耗性能(全量备份,消耗IO);不可控,可能存在丢失数据情况(如进程被kill掉)。

  • 写日志(AOF)

保存的是客户端执行的命令,存储为AOF文件;装载的过程就是读取AOF文件去执行命令。它是增量备份的。

AOF的三种策略:

        1. always:写命令刷新缓冲区 -> 每条命令fsync到磁盘 -> AOF文件;不丢失数据,IO开销大,一般sata盘只有几百TPS。

        2. everysec:写命令刷新缓冲区 -> 每秒把缓冲区fsync到磁盘 -> AOF文件;每秒一次fsync,丢失1秒数据。

        3. no:写命令刷新缓冲区 -> 操作系统决定,缓冲区fsync到磁盘 -> AOF文件;不可控。

AOF重写:

redis在AOF操作时,会把相关的命令进行优化、重写,减少命令的次数。 

相关配置:

# 开启AOF
appendonly no
# 备份文件
appendfilename "appendonly.aof"
# AOF策略
appendfsync everysec
# 重写的时候是否执行AOF操作
no-appendfsync-on-rewrite no
# 自动重写的百分比
auto-aof-rewrite-percentage 100
# 重写的大小
auto-aof-rewrite-min-size 64mb
  • 比较

RDB最佳策略:

        1. 在实际工作中,建议关闭RDB,同时开启主从;

        2. 集中管理:按天,按小时备份数据,不要使用自动配置的策略;

        3. 主从配置,从节点打开;

AOF最佳策略:

        1. 开缓存和存储,大部分情况都打开;

        2. AOF重写集中管理;

        3. everysec策略,通过每秒刷新备份;

        4. 在重写的时候结合RDB备份进行快照;

混合持久化内容的AOF文件:

# 开启AOF
appendonly yes
# 开启混合模式
aof-use-rdb-preamble yes

高可用

单机环境可能出现单点故障、吞吐量有限和存储容量不够的情况。

主从复制

一个maser可以有多个slave;

一个slave只能有一个master;

数据流向是单向的,只能从master流向slave。

实现原理 

1. 建立连接阶段

1.1 保存主节点信息

slave节点保存master节点的ip和port信息,即masterhost和masterport字段;

slaveof是异步命令,slave保存了master的节点信息后,该命令直接返回ok,实际的复制从这之后开始。

1.2 建立socket连接

slave节点每秒1次调用复制定时函数replicationCron(),发现有master节点,则根据ip和port创建socket连接。

slave节点:为该socket创建一个专门处理复制工作的文件事件处理器,负责后续的事件处理工作,如接收RDB文件、接收文件传播等。

master节点:创建socket对应的客户端状态,将slave节点看作是连接到master节点的一个客户端,后续操作有slave节点向master节点发送命令请求来进行。

1.3 发送ping命令

slave节点发送ping命令,检查socket连接是否可用,以及master节点当前能否处理请求。

1.4 身份验证

如果slave节点设置了masterauth选项,则slave节点需要向master节点进行身份认证,如果未设置,则不需要进行验证。

1.5 发送slave节点端口信息

slave将其监听的端口发送给master节点,master保存该节点端口到对应客户端的slave_listening_port字段;该字段仅显示,无其他作用。

2. 数据同步阶段

slave节点向master节点发送psync命令开始同步,同步数据可以分为全量复制和部分复制。

3. 命令传播阶段

master节点将自己执行的写命令发送给slave节点,slave节点接收命令并执行,保证数据的一致性。

除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。

命令传播是异步过程,即master节点不会等待slave节点的回复,数据不一致的程度,与主从节点之间的网络状况、主节点的写命令执行频率,以及主节点的repl-disable-tcp-nodelay配置等有关。

repl-disable-tcp-nodelay:该命令作用于命令传播阶段,控住master节点是否禁止与slave节点TCP_NODELAY,默认no;当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率和linux的内核有关,默认配置40ms;当设置为no时,TCP会立即将master节点的数据发送给slave节点,带宽增加但数据延迟变小。

一般来说,只有当应用对Redis一致性容忍度较高,且主从节点之间的网络状况不好时,才会设为yes;多数情况使用默认的no。

复制

redis2.8及之后,slave可以发送psync命令请求同步数据,根据主从节点当时状态的不同,同步方式可能是全量复制或部分复制。

  • 全量复制

用于初次复制或其他无法部分复制的情况,将master的数据全部发送给slave节点。

  • 部分复制

用于网络中断等情况的复制,只将中断期间的master执行的写命令发送给slave,更加高效;如果中断时间较长,master没有保存中断期间完整的写命令,则仍使用全量复制。

        复制偏移量

        主节点和从节点各自维护一个offset,代表的是主节点向从节点传递数据的字节数,主节点向从节点传播n个字节数时,主节点offset+n,从节点接收n个字节数时,从节点offset+n。

        offset用于判断主从节点数据库状态是否一致,若相等,则一致,若不同,则不一致,需要根据两个offset找出缺失的那部分数据;如果主节点的offset是n+m,从节点offset是n,那么部分复制就只需要吧n+1 ~ m的数据传递给从节点。

        复制积压缓冲区

        由主节点维护、固定长度的、FIFO队列,默认1MB,当主节点开始有从节点时创建,作用是备份主节点最近发送给从节点的数据,无论有多少个从节点,复制积压缓冲区只有一个。

        在命令传播阶段,主节点除了将写命令发送给从节点,也会发送一份到积压缓冲区,复制积压缓冲区还存储了每个字节对应的offset,因为定长且先进先出,时间较早的写命令会被挤出缓冲区。

        复制积压缓冲区可以备份的写命令有限,当主从节点offset差距过大超出缓冲区长度时,无法进行部分复制。repl-backlog-size配置缓冲区大小。如果需要复制的offset偏移量在积压缓冲区内,则进行部分复制,否则进行全量复制。

        服务器运行id(runid)

        每个redis节点(不分主从)在启动时都会生成一个随机ID(由40个随机的十六进制字符组成),runid用来唯一识别一个redis节点。(debug reload重启:重启后,主节点的runid和offset都不受影响,但会清空当前内存中的数据,重新从RDB文件中加载,这个过程会导致主节点的阻塞。)

        主从节点初次复制时,主节点会将其runid发送给从节点存储起来,当断线重连时,从节点会将该runid发送给主节点,主节点判断是否进行部分复制:

        1. 如果从节点保存的runid和主节点现在的runid一致,说明之前进行过主从同步,则会继续尝试是否进行部分复制(取决于offset和复制积压缓冲区);

        2. 如果从节点保存的runid和主节点现有的runid不一致,说明该从节点断线前同步的redis节点并不是当前主节点,只能进行全量复制。

作用

  • 单点故障;
  • 读写分离(master写,slave读);
  • 一主多从;
  • 多副本;

复制配置

  • slaveof

        在需要作为slave的机器上执行命令 slaveof ip port 角色变为slave,会将本身已有的数据删除,完全同步master的数据。

        slaveno one 关闭slave角色,重新作为master角色。

  • 配置文件方式

        文件中配置 replicaof ip port,启动时作为slave。

Redis Sentinel

主从复制高可用问题

1. 若master故障,需要手动故障转移。

2. 写能力和存储能力受限。

哨兵机制解决问题1,在master故障时,自动选举出一个新的master,而客户端不感知;如果旧master服务恢复,则会重新作为slave加入。

配置开启sentinel

1. 配置开启主从节点:redis-server redis.conf;

daemonize yes
bind 0.0.0.0
port 端口
dir /user/local/data
logfile /user/local/6379.log
# 作为slave绑定哪个master
replicaof ip port

2. 配置开启sentinel监控节点:redis-sentinel sentinel.conf;

port 23456
dir /usr/local/redis/data/
logfile 23456.log
# 告诉sentinel去监听地址为ip:port的一个master,这里的master-name可以自定义,quorum是一个数字,指明当有多少个sentinel认为一个master失效时,master才真正失效
sentinel monitor mymaster 127.0.0.1 7000 2
# 指定了需要多少失效时间,一个master才会被这个sentinel主观的认为不可用,单位是毫秒,默认30s
sentinel down-after-milliseconds mymaster 30000
指定在发生failover主备切换时,最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover所需的时间就越长,但如果这个数字越大,就意味着越多的slave因为replication而不可用;可以通过将这个值设为1来保证每次只有一个slave处于不能处理命令请求的状态
sentinel parallel-syncs mymaster 1
# 失效之后多久重新检测一次master状态
sentinel failover-timeout mymaster 180000

客户端测试

    public void test(){
        String master = "mymaster";
        Set<String> sentinels = new HashSet<>();
        sentinels.add("127.0.0.1:23456");
        sentinels.add("127.0.0.1:23457");
        sentinels.add("127.0.0.1:23458");
        JedisSentinelPool pool = new JedisSentinelPool(master, sentinels);

        while (true){
            Jedis jedis = null;
            try {
                jedis = pool.getResource();
                jedis.incr("age");
                System.out.println(jedis.get("age"));
                TimeUnit.SECONDS.sleep(1);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

Redis Cluster

解决

1. 存储容量不够情况;

2. 写数据QPS提高;

3. 提高网络流量。

数据分布

数据分片

分区规则

根据key的规则来分区

  • 顺序分区

1. 数据容易倾斜;

2. 键值分布业务相关;

3. 支持批量操作;

4. 可以顺序访问。

  • Hash分区

1. 数据分散度高;

2. 键值分布与业务无关;

3. 支持批量操作;

4. 无法顺序访问。

Hash分布

  • 节点取余扩容

客户端分片。

在使用Hash取余运算的时候,当对服务器扩容时,比如由原来的3个服务扩容到4个服务节点时,会有90%的数据需要迁移操作;如果需要保证数据较少的迁移,可以考虑翻倍扩容,如3个节点扩容至6个节点,需要迁移的数据为50%。 

  • 一致性Hash

客户端分片。

采用Hash环的概念,在一个环上有0 - 2^32个数值,在环上均匀分配4个服务节点,每个服务节点负责对应的一片区域,当插入一个key时,首先对key进行Hash值运算,计算出在环上的对应位置,然后把对应的数据顺时针找到对应的存储节点,进行数据的存储和读取。

当插入一个新的服务节点5时,其中只会影响节点2负责的节点资源,需要把节点2负责的部分数据迁移(不会自动迁移)到节点5。

客户端分片:Hash+顺时针(服务端不限制插入,由客户端计算位置找到对应的节点);

节点伸缩:扩容或者删除只会影响相邻节点,不会影响其他节点;

翻倍伸缩:保证最小迁移和负载均衡;

  • 虚拟槽分配 

服务端分片。对于所有的服务节点,都知道集群中的哪些服务器节点负责哪些slot(槽)。槽的大小固定13686,服务节点不固定。

当客户端连接一个Node1服务节点时,进行插入数据,首先对key进行Hash运算,计算出对应的Hash值(0-65536),然后把Hash和13686进行与运算,得出的结果值(0-13686),服务端接收到对应的请求,如果在当前服务节点(不固定哪个节点),直接进行数据操作,如果不在当前服务节点,那么会连接到对应的服务节点进行数据操作。

搭建高可用集群

  • 原生命令安装

1. 启动多个集群节点服务器;

集群配置参数 - 6个节点

port 7000
# 启用集群
cluster-enabled yes
# 保存节点配置文件的路径
cluster-config-file nodes.conf
# 集群节点通信超时时间,毫秒
cluster-node-timeout 5000
# 当节点出现问题的时候是否不让整个集群提供服务
cluster-require-full-coverage no

2. 使用meet操作,让各个服务器之间感知;

# 本节点和指定节点互相通信
> cluster meet ip port

3. 需要给每个master服务器指定slot,分配槽位;

# 分配槽位,需要一个一个指定,可通过脚本操作
> redis-cli -h {ip} -p {port} cluster addslots {slot}

4. 给master节点指定复制节点。

# 指定是哪个节点的slave服务
> redis-cli -h {slave_ip} -p {slave_port} cluster replicate {node_id}
# 查看集群节点信息,包含id信息
> cluster nodes
  • 工具安装

redis5及以上支持。

1. 启动集群节点;

2. 通过命令进行安装

# --cluster-replicas 1 表示主从比例1:1
# --cluster-replicas 2 表示主从比例1:2
> redis-cli --cluster create {node1_ip:port} {node2_ip:port} {...} --cluster-replicas 1

一些扩展

常用Java客户端

jedis 接近命令行操作

redission 异步和非阻塞的api(netty框架)

lettuce 异步和非阻塞的api(netty框架)

常见问题

  • 穿透

指查询一个一定不存在的数据,由于缓存是不命中的,需要从数据库查询,查不到数据不写入缓存,这将导致这个不存在的数据每次都需要去查询数据库,造成缓存穿透。

解决方案:

1. 单个key穿透

持久层查询不到就缓存空结果,查询时先判断缓存中是否存在,如果有直接返回空,没有则查询后返回,对于空值,设置一个较小的缓存过期时间。

2. 多个key穿透

先判断key是否是一个存在的key,如果key是一个合法的key,那么就查询数据,如果key不合法,那么就不查数据库。

数据量不大,可以把key都存放到redis的set集合中;数据量非常大时(上亿级别),可以考虑使用布隆过滤器来完成。

  • 雪崩

如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落到了数据库上,造成数据库服务器的雪崩。

解决方案:

1. 让key的过期时间均匀分布;

2. 限流 - 控制同一个key的线程数量(读取数据库的线程数量),常见的限流算法有:计数算法,滑动窗口,令牌桶,漏桶。

  • 热点key

某个key的访问非常频繁,导致redis服务器的压力剧增,没法保证redis可以正常提供服务。

因为是redis服务器的压力导致瘫痪的,所以解决方法主要集中在提高redis的qps和控制redis的访问。

解决方案:

1. 扩容,添加集群中的从服务器,提高读数据能力;

2. 考虑使用本地缓存,把redis中的热点数据直接缓存在本地的服务器内存中,减少对redis的一个访问。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值