2redis进阶

Redis进阶

Redis单线程模型

  • redis 会将每个客户端都关联一个指令队列。客户端的指令通过队列来按顺序处理,先到先服务。
  • 在一个客户端的指令队列中的指令是顺序执行的,但是多个指令队列中的指令是无法保证顺序的,例如执行完 client-0 的队列中的 command-0 后,接下去是执行哪个队列中的第一个指令是无法确定的,但是肯定不会同时执行两个指令。
  • redis 同样也会为每个客户端关联一个响应队列,通过响应队列来顺序地将指令的返回结果回复给客户端。
  • 同样,一个响应队列中的消息可以顺序的回复给客户端,多个响应队列之间是无法保证顺序的。
  • 所有的客户端的队列中的指令或者响应,redis 每次都只能处理一个,同一时间绝对不会处理超过一个指令或者响应。
  • 利用了NIO模型保证了IO效率,使用单线程保证了运行效率

redis分布式锁

  • 需要设置一个过期时间,防止锁住后挂掉了,导致锁无法别释放。

分布式锁注意事项

  • 互斥性:在任意时刻,只有一个客户端能持有锁
  • 同一性:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  • 避免死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

实现分布式锁

获取锁

  • 在SET命令中, 有很多选项可用来修改命令的行为。 以下是SET命令可用选项的基本语法。
    1
    redis 127.0.0.1:6379> SET KEY VALUE [EX seconds][PX milliseconds][NX|XX]
    • EX seconds一设置指定的到期时间(以秒为单位)。
    • PX milliseconds-设置指定的到期时间(以毫秒为单位)。
    • NX-仅在键不存在时设置键。
    • xx-只有在键已存在时才设置。
  • 方式1:用set方式实现
    1
    2
    3
    4
    5
    6
    7
    8
    public static boolean getLock(String lockKey,String requestId,int expireTime) {
    //NX:保证互斥性
    String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
    if("OK".equals(result)) {
    return true;
    }
    return false;
    }
  • 方式2:用setnx实现
    1
    2
    3
    4
    5
    6
    7
    8
    public static boolean getLock(String lockKey,String requestId,int expireTime) {
    Long result = jedis.setnx(lockKey, requestId);
    if(result == 1) {
    jedis.expire(lockKey, expireTime);
    return true;
    }
    return false;
    }

释放锁

  • 方式1:del实现
    1
    2
    3
    4
    5
    public static void releaseLock(String lockKey,String requestId) {
    if (requestId.equals(jedis.get(lockKey))) {
    jedis.del(lockKey);
    }
    }
  • 方式2: redis+lus脚本实现
    1
    2
    3
    4
    5
    6
    7
    8
    public static boolean releaseLock(String lockKey, String 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 (result.equals(1L)) {
    return true;
    }
    return false;
    }

利用redisson实现分布式锁

  • 依赖redisson
  • 尝试从 N 个互相独立 Redis 实例获取锁;
  • 计算获取锁消耗的时间,只有时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,才认为获取锁成功;
  • 如果获取锁失败,就到每个实例上释放锁。

持久化

RDB(默认)

  • 默认,通过快照的方式完成持久化,当满足一定的条件时,Redis会自动j将内存中的数据进行快照并持久化到硬盘
    • 涉及的原理:fork 和 copyonwrite
  • 指定的情况
    • 符合自定义配置的快照规则(在redis.conf中设置快照规则
    • 执行save或者bgsave命令
    • 执行flushall命令
    • 执行主从复制操作
  • 配置文件持久化设置
    • after 900 sec (15 min) if at least 1 key changed
      • save 900 1
    • after 300 sec (5 min) if at least 10 keys changed
      + save 300 10
    
    • after 60 sec if at least 10000 keys changed
      + save 60 10000
    
    • 指定rdb快照文件的名称

      • dbfilename dump.rdb
    • 启动时要指定配置文件,持久化会生成rdb后缀的文件,可以配置多个条件,条件之间用或来做逻辑判断

    • 特别说明

      • Redis启动后会读取快照文件,将数据从硬盘中载入
      • 通常将记录一千万个字符串类型的键,大小为1GB的快照文件载入到内存中需要花费20-30秒
  • 优缺点
    • 使用RDB方式实现持久化,一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。这个时候我们就需要根据具体的应用场景,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受范围。如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化
    • RDB可以最大化Redis的性能:父进程在保存RDB文件时唯一要做的就是fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无序执行任何磁盘I/O操作。同时这个也是一个缺点,如果数据集比较大的时候,fork可以能比较耗时,造成服务器在一段时间内停止处理客户端的请求;

AOF

  • 日志记录的方式,可以记录每一条命令的操作,可以每一次命令操作后,持久化数据
  • 开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件,这一过程显然会降低Redis的性能,但大部分情况下这个影响是能够接受的,另外使用较快的硬盘可以提高AOF的性能
  • 配置文件
    • 开启aof: appendonly yes
    • 同步的频率: appendfsync
    • no-appendfsync-on-rewrite yes 解决当主进程和bgrewriteaof同时操作aof文件的阻塞问题,但是可能会丢失数据,相当于将appendfsync设置为no.
    • aof-use-rdb-preamble yes 当重写AOF时,使用RDB的方式,4.0版本后的功能,AOF和RDB混合使用

同步磁盘数据

  • Redis每次更改数据的时候, aof机制都会将命令记录到aof文件,但是实际上由于操作系统的缓存机制,数据并没有实时写入到硬盘,而是进入硬盘缓存。再通过硬盘缓存机制去刷新到保存到文件
    • appendfsync always 每一次操作都进行持久化
    • appendfsync everysec 每隔一秒都持久化
    • appendfsync no 当buffer满时才刷新到aof的磁盘中,可能会丢失一次buffer中的操作,no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。

AOF重写

  • 命令 : bgrewriteaof 用于异步执行一个 AOF(AppendOnly File) 文件重写操作
  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写
  • 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。
  • 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
  • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松
  • 重写命令:
    • #auto-aof-rewrite-percentage 100 表示当前aof文件大小超过上一次aof文件大小的百分之多少的时候会进行重写。如果之前没有重写过,以启动时aof文件大小为准
    • #auto-aof-rewrite-min-size 64mb 限制允许重写最小aof文件大小,也就是文件大小小于64mb的时候,不需要进行优化

AOF损坏后如何修复

  • 服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏。
  • 当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:
    1. 为现有的 AOF 文件创建一个备份。
    2. 使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复。redis-check-aof –fix
    3. 重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。

如何选择RDB和AOF

  • 一般来说,如果对数据的安全性要求非常高的话,应该同时使用两种持久化功能。
  • 如果可以承受数分钟以内的数据丢失,那么可以只使用 RDB 持久化。
  • 有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快 。
  • 两种持久化策略可以同时使用,也可以使用其中一种。如果同时使用的话, 那么Redis重启时,会优先使用AOF文件来还原数据

Redis4.0混合持久化

  • rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小

  • redis4_0

主从复制

  • 持久化保证了即使redis服务重启也不会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当redis服务器的硬盘损坏了可能会导致数据丢失,不过通过redis的主从复制机制就可以避免这种单点故障

  • 主redis中的数据有两个副本(replication)即从redis1和从redis2,即使一台redis服务器宕机其它两台redis服务也可以继续提供服务。

  • 主redis中的数据和从redis上的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上。

  • 只有一个主redis,可以有多个从redis。

  • 主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求

  • 一个redis可以即是主又是从

  • 主从复制在没有哨兵机制下,主机redis出问题后只能人工处理

主从配置

  • master无需配置
  • slave配置及需要注意的点
    • slaveof 5.0使用replicaof
    • masterauth 指定master认证时的密码
    • 修改bind
    • 主节点保护模式关闭并且设置密码
    • replica-serve-statle-data yes : 是否同步完才向外暴露数据
    • replica-read-only yes : 是否开启只读模式
    • repl-diskless-sync : 同步时是否使用磁盘,如果为否则使用网络
    • repl-backing-size 1mb : 增量复制的队列大小

分片

  • 分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。
  • 假设有 4 个 Redis 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,… ,有不同的方式来选择一个指定的键存储在哪个实例中。
    • 最简单的方式是范围分片,例如用户 id 从 01000 的存储到实例 R0 中,用户 id 从 10012000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
    • 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
  • 根据执行分片的位置,可以分为三种分片方式:
    • 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
    • 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
    • 服务器分片:Redis Cluster。

Bloom过滤器

  • 它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

Bloom原理

  • 当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

使用

  • 使用
    • github地址:https://github.com/RedisBloom/RedisBloom
    • 下载源码并make
    • 复制redisbloom.so文件到任意位置
    • redis-server –loadmodule /path/to/redisbloom.so (注意此处使用绝对路径) /etc/redis-config.conf
  • 作用:使用bitmap解决缓存穿透问题
  • 命令:
    • 新增:bf.add

应用场景

  • 防止缓存穿透
  • 爬虫过滤已抓到的url就不再抓,可用bloom filter过滤
  • 垃圾邮件过滤

Bloom缺点

  • 删除困难:
  • 存在误判:可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素。
  • 匹配结果只能是“绝对不在集合中”,并不能保证匹配成功的值已经在集合中。

Redis和LUA整合

LUA

  • Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能

Redis使用LUA的好处

  • 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行
  • 原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说,编写脚本的过程中无需担心会出现竞态条件
  • 复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑

2redis进阶
https://x-leonidas.github.io/2022/02/01/05数据库/05-2非关系型数据库/redis/2redis进阶/
作者
听风
发布于
2022年2月1日
更新于
2025年4月29日
许可协议