关于redis:万字好文带你入门-redis

5次阅读

共计 33208 个字符,预计需要花费 84 分钟才能阅读完成。

引言|本文将会从:Redis 应用场景与介绍 -> 数据结构与简略应用 -> 小性能大用处 -> 长久化、主从同步与缓存设计 -> 常识拓展 来书写,初学的童鞋只有能记住 Redis 是用来干嘛,各性能的应用场景有哪些,而后对 Redis 有个大略的意识就好啦,剩下的当前有须要的时候再来查看和实际吧!

一、Redis 介绍

Redis 是什么?

Redis 是一个开源(BSD 许可)的,内存中的数据结构存储系统,它能够用作数据库、缓存和消息中间件。Redis 反对多种类型的数据结构,如 字符串(strings),散列(hashes),列表(lists),汇合(sets),有序汇合(sorted sets),范畴查问,bitmaps,hyperloglogs 和 天文空间(geospatial)索引半径查问。Redis 内置了复制(replication),LUA 脚本(Lua scripting),LRU 驱动事件(LRU eviction),事务(transactions)和不同级别的 磁盘长久化(persistence),Redis 通过 哨兵(Sentinel)和主动分区(Cluster)提供高可用性(high availability)。

Redis 个性

  • 速度快
    • 单节点读 110000 次 /s,写 81000 次 /s
    • 数据寄存内存中
    • 用 C 语言实现,离操作系统更近
    • 单线程架构,6.0 开始反对多线程(CPU、IO 读写负荷)
  • 长久化
    • 数据的更新将异步地保留到硬盘(RDB 和 AOF)
  • 多种数据结构
    • 不仅仅反对简略的 key-value 类型数据,还反对:字符串、hash、列表、汇合、有序汇合,
  • 反对多种编程语言
  • 功能丰富
    • HyperLogLog、GEO、公布订阅、Lua 脚本、事务、Pipeline、Bitmaps,key 过期
  • 简略稳固
    • 源码少、单线程模型
  • 主从复制
  • Redis 反对数据的备份(master-slave)与集群(分片存储),以及领有哨兵监控机制。
  • Redis 的所有操作都是原子性的,同时 Redis 还反对对几个操作合并后的原子性执行。

Redis 典型应用场景

缓存:

计数器:

音讯队列:

排行榜:

社交网络:

Redis 高并发原理

  1. Redis 是纯内存数据库,个别都是简略的存取操作,线程占用的工夫很多,工夫的破费次要集中在 IO 上,所以读取速度快
  2. Redis 应用的是非阻塞 IO,IO 多路复用,应用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,缩小了线程切换时上下文的切换和竞争。
  3. Redis 采纳了单线程的模型,保障了每个操作的原子性,也缩小了线程的上下文切换和竞争。
  4. Redis 存储构造多样化,不同的数据结构对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,应用有序的数据结构放慢读取的速度。
  5. Redis 采纳本人实现的事件分离器,效率比拟高,外部采纳非阻塞的执行形式,吞吐能力比拟大。

Redis 装置

这里只提供 linux 版本的装置部署

  • 下载 Redis

进入官网找到下载地址:https://redis.io/download

右键 Download 按钮,抉择复制链接地址,而后进入 linux 的 shell 控制台:输出 wget 将下面复制的下载链接粘贴上,如下命令:

wget https://download.redis.io/releases/redis-6.2.4.tar.gz

回车后期待下载结束。

  • 解压并装置 Redis

下载实现后须要将压缩文件解压,输出以下命令解压到当前目录:

tar -zvxf redis-6.2.4.tar.gz

解压后在根目录上输出 ls 列出所有目录会发现与下载 redis 之前多了一个 redis-6.2.4.tar.gz 文件和 redis-6.2.4 的目录。

  • 挪动 Redis 目录(可选)

若你不想在下载的目录装置 Redis,能够将 Redis 挪动到特定目录装置,我习惯放在‘/usr/local/’目录下,所以我这里输出命令将目前在‘/root’目录下的 ‘redis-6.2.4’ 文件夹更改目录,同时批改其名字为 redis:

mv /root/rredis-6.2.4 /usr/local/redis

cd 到 ‘/usr/local’ 目录下输出 ls 命令能够查问到当前目录曾经多了一个 redis 子目录,同时 ‘/root’ 目录下曾经没有 ‘redis-6.2.4’ 文件:

  • 编译

cd 到 ‘/usr/local/redis’ 目录,输出命令 make 执行编译命令,接下来控制台会输入各种编译过程中输入的内容:

make

最终运行后果如下:

  • 装置

输出以下命令:

make PREFIX=/usr/local/redis install

这里多了一个关键字 ‘PREFIX=’ 这个关键字的作用是编译的时候用于指定程序寄存的门路。比方咱们当初就是指定了 redis 必须寄存在 ‘/usr/local/redis’ 目录。假如不增加该关键字 linux 会将可执行文件寄存在 ‘/usr/local/bin’ 目录,库文件会寄存在 ‘/usr/local/lib’ 目录。配置文件会寄存在 ‘/usr/local/etc 目录。其余的资源文件会寄存在 ‘usr/local/share’ 目录。这里指定好目录也不便后续的卸载,后续间接 rm -rf /usr/local/redis 即可删除 Redis。执行后果如下图:

到此为止,Redis 曾经装置结束,能够开始应用了~

Redis 启动

依据下面的操作曾经将 redis 装置实现了。在目录‘/usr/local/redis’输出上面命令启动 redis:

./bin/redis-server& ./redis.conf

下面的启动形式是采取后盾过程形式, 上面是采取显示启动形式(如在配置文件设置了 daemonize 属性为 yes 则跟后盾过程形式启动其实一样):

./bin/redis-server ./redis.conf

两种形式区别无非是有无带符号 & 的区别。redis-server 前面是配置文件,目标是依据该配置文件的配置启动 redis 服务。redis.conf 配置文件容许自定义多个配置文件,通过启动时指定读取哪个即可。启动能够概括为:

  • 最简默认启动
    • 装置后在 bin 目录下间接执行 redis-server
    • 验证(ps –aux | grep redis)
  • 动静参数启动(可配置一下参数,例如指定端口)
    • ./bin/redis-server –port 6380
  • 配置文件启动
    • ./bin/redis-server& ./redis.conf
  • 生产环境个别抉择配置启动
  • 单机多实例配置文件能够用端口辨别开

注:若在进行 redis 命令操作,间接在 redis 中的 bin 目录下运行 redis-cli 命令即可,若开启了多个则须要加上对应的端口参数:若运行 redis-cli 提醒不未装置,则装置一下即可:

redis.conf 配置文件

在目录 ‘/usr/local/redis’ 下有一个 redis.conf 的配置文件。咱们下面启动形式就是执行了该配置文件的配置运行的。咱们能够通过 cat、vim、less 等 linux 内置的读取命令读取该文件。这里列举下比拟重要的配置项:配置项名称配置项值范畴阐明

daemonize yes、no yes 示意启用守护过程,默认是 no 即不以守护过程形式运行。其中 Windows 零碎下不反对启用守护过程形式运行
port 指定 Redis 监听端口,默认端口为 6379
bind 绑定的主机地址, 如果须要设置近程拜访则间接将这个属性备注下或者改为 bind * 即可, 这个属性和上面的 protected-mode 管制了是否能够近程拜访
protected-mode yes、no 保护模式,该模式管制内部网是否能够连贯 redis 服务,默认是 yes, 所以默认咱们外网是无法访问的,如需外网连贯 rendis 服务则须要将此属性改为 no
loglevel debug、verbose、notice、warning 日志级别,默认为 notice
databases 16 设置数据库的数量,默认的数据库是 0。整个通过客户端工具能够看失去
rdbcompression yes、no 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采纳 LZF 压缩,如果为了节俭 CPU 工夫,能够敞开该选项,但会导致数据库文件变得微小
dbfilename dump.rdb 指定本地数据库文件名,默认值为 dump.rdb
dir 指定本地数据库寄存目录
requirepass 设置 Redis 连贯明码,如果配置了连贯明码,客户端在连贯 Redis 时须要通过 AUTH 命令提供明码,默认敞开
maxclients 0 设置同一时间最大客户端连接数,默认无限度,Redis 能够同时关上的客户端连接数为 Redis 过程能够关上的最大文件描述符数,如果设置 maxclients 0,示意不作限度。当客户端连接数达到限度时,Redis 会敞开新的连贯并向客户端返回 max number of clients reached 错误信息
maxmemory XXX 指定 Redis 最大内存限度,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试革除已到期或行将到期的 Key,当此办法解决 后,依然达到最大内存设置,将无奈再进行写入操作,但依然能够进行读取操作。Redis 新的 vm 机制,会把 Key 寄存内存,Value 会寄存在 swap 区。配置项值范畴列里 XXX 为数值

这里我要将 daemonize 改为 yes,不然我每次启动都得在 redis-server 命令前面加符号 &,不这样操作则只有回到 linux 控制台则 redis 服务会主动敞开,同时也将 bind 正文,将 p rotected-mode 设置为 no。这样启动后我就能够在外网拜访了。批改形式通过 vim 或者你喜爱的形式即可:

vim /usr/local/redis/redis.conf

通过 /daemonize  查找到属性,默认是 no,更改为 yes 即可。(通过 / 关键字查找呈现多个后果则应用 n 字符切换到下一个即可,按 i 能够开始编辑,ESC 退出编辑模式,输出 :wq 命令保留并退出),如下图:其余属性也是同样形式查找和编辑即可。
装置部署局部参考:https://www.cnblogs.com/hunan…
二、Redis 数据结构与命令应用
Redis 的数据结构有:string(字符串)、hash(哈希)、list(列表)、set(汇合)、zset(有序集 合)。但这些只是 Redis 对外的数据结构,实际上每种数据结构都有本人底层的外部编码实现,而且是多种实现,这样 Redis 会在适合的场景抉择适合的外部编码。

能够看到每种数据结构都有两种以上的外部编码实现,例如 list 数据结 构蕴含了 linkedlist 和 ziplist 两种外部编码。同时,有些外部编码,例如 ziplist,能够作为多种内部数据结构的外部实现,能够通过 object encoding 命令查问外部编码。

object encoding xxx  # xxx 为键名

Redis 所有的数据结构都是以惟一的 key 字符串作为名称,而后通过这个惟一 key 值来获取相应的 value 数据。不同类型的数据结 构的差别就在于 value 的构造不一样。

通用全局 命令

罕用全局命令
  • keys:查看所有键
  • dbsize:键总数
  • exists key:查看键是否存在
  • del key [key …]:删除键
  • expire key seconds:键过期
  • ttl key: 通过 ttl 命令察看键键的残余过期工夫
  • type key:键的数据结构类型
简略应用截图

依据下面的命令解释,大家应该比拟容易看懂截图外面的所有命令含意,这里就不过多解释了。

字符串应用

字符串 string 是 Redis 最简略的数据结构。Redis 的字符串是动静字符串,是能够批改的字符串,内部结构实现上相似于 Java 的 ArrayList,采纳预调配冗余空间的形式来缩小内存的频繁调配。字符串构造应用十分宽泛,一个常见的用处就是缓存用户信息。咱们将用户信息结构体 应用 JSON 序列化成字符串,而后将序列化后的字符串塞进 Redis 来缓存。同样,取用户 信息会通过一次反序列化的过程。

罕用字符串命令
  • set key value [ex seconds] [px milliseconds] [nx|xx]: 设置值,返回 ok 示意胜利
    • ex seconds: 为键设置秒级过期工夫。
    • px milliseconds: 为键设置毫秒级过期工夫。
    • nx: 键必须不存在,才能够设置胜利,用于增加。可独自用 setnx 命令代替
    • xx: 与 nx 相同,键必须存在,才能够设置胜利,用于更新。可独自用 setxx 命令代替
  • get key:获取值
  • mset key value [key value …]:批量设置值,批量操作命令能够无效进步业务解决效率
  • mget key [key …]:批量获取值,批量操作命令能够无效进步业务解决效率
  • incr key:计数,返回后果分 3 种状况:
    • 值不是整数,返回谬误。
    • 值是整数,返回自增后的后果。
    • 键不存在,依照值为 0 自增,返回后果为 1。
  • decr(自减)、incrby(自增指定数字)、decrby(自减指定数字)
字符串简略应用截图

依据下面的命令解释,大家应该比拟容易看懂截图外面的所有命令含意,这里就不过多解释了。

字符串应用场景
  1. 缓存数据,进步查问性能。比方存储登录用户信息、电商中存储商品信息
  2. 能够做计数器(想晓得什么时候封闭一个 IP 地址(拜访超过几次)), 短信限流
  3. 共享 Session,例如:一个分布式 Web 服务将用户的 Session 信息 (例如用户登录信息) 保留在各自服务器中,这样会造成一个问题,出于负载平衡的思考,分布式服务会将用户的拜访平衡到不同服务器上,用户刷新一次拜访可 能会发现须要从新登录,为了解决这个问题,能够应用 Redis 将用户的 Session 进行集中管理,在这种模式下只有保障 Redis 是高可用和扩展性的,每次用户 更新或者查问登录信息都间接从 Redis 中集中获取,如图:

哈希 hash

哈希相当于 Java 中的 HashMap,以及 Js 中的 Map,外部是无序字典。实现原理跟 HashMap 统一。一个哈希表有多个节点,每个节点保留一个键值对。与 Java 中的 HashMap 不同的是,rehash 的形式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,须要一次性全副 rehash。Redis 为了高性能,不能梗塞服务,所以采纳了渐进式 rehash 策略。渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 构造,查问时会同时查问两个 hash 构造,而后在后续的定时工作中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁徙到新的 hash 构造中。当搬迁实现了,就会应用新的 hash 构造取而代之。当 hash 移除了最初一个元素之后,该数据结构主动被删除,内存被回收。

罕用哈希命令
  • hset key field value:设置值
  • hget key field:获取值
  • hdel key field [field …]:删除 field
  • hlen key:计算 field 个数
  • hmset key field value [field value …]:批量设置 field-value
  • hmget key field [field …]:批量获取 field-value
  • hexists key field:判断 field 是否存在
  • hkeys key:获取所有 field
  • hvals key:获取所有 value
  • hgetall key:获取所有的 field-value
  • incrbyfloat 和 hincrbyfloat: 就像 incrby 和 incrbyfloat 命令一样,然而它们的作 用域是 filed
哈希简略应用截图

依据下面的命令解释,大家应该比拟容易看懂截图外面的所有命令含意,这里同样不过多解释了

哈希应用场景
  1. Hash 也能够同于对象存储,比方存储用户信息,与字符串不一样的是,字符串是须要将对象进行序列化(比方 json 序列化)之后能力保留,而 Hash 则能够讲用户对象的每个字段独自存储,这样就能节俭序列化和反序列的工夫。如下:
  1. 此外还能够保留用户的购买记录,比方 key 为用户 id,field 为商品 i d,value 为商品数量。同样还能够用于购物车数据的存储,比方 key 为用户 id,field 为商品 id,value 为购买数量等等:

列表(lists)

Redis 中的 lists 相当于 Java 中的 LinkedList,实现原理是一个双向链表(其底层是一个疾速列表),即能够反对反向查找和遍历,更不便操作。插入和删除操作十分快,工夫复杂度为 O(1),然而索引定位很慢,工夫复杂度为 O(n)。

罕用列表命令
  • rpush key value [value …]:从左边插入元素
  • lpush key value [value …]:从右边插入元素
  • linsert key before|after pivot value:向某个元素前或者后插入元素
  • lrange key start end:获取指定范畴内的元素列表,lrange key 0 -1能够从左到右获取列表的所有元素
  • lindex key index:获取列表指定索引下标的元素
  • llen key:获取列表长度
  • lpop key:从列表左侧弹出元素
  • rpop key:从列表右侧弹出
  • lrem key count value:删除指定元素,lrem 命令会从列表中找到等于 value 的元素进行删除,依据 count 的不同 分为三种状况:
    • ·count>0,从左到右,删除最多 count 个元素。
    • count<0,从右到左,删除最多 count 绝对值个元素。
    • count=0,删除所有。
  • ltrim key start end:依照索引范畴修剪列表
  • lset key index newValue:批改指定索引下标的元素
  • blpop key [key …] timeout 和 brpop key [key …] timeout:阻塞式弹出
列表简略应用截图

依据下面的命令解释,大家应该比拟容易看懂截图外面的所有命令含意,这里同样不过多解释了

列表应用场景
  1. 热销榜,文章列表
  2. 实现工作队列(利用 lists 的 push 操作,将工作存在 lists 中,而后工作线程再用 pop 操作将工作取出进行执行),例如音讯队列
  3. 最新列表,比方最新评论

应用参考:

  • lpush+lpop=Stack(栈)
  • lpush+rpop=Queue(队列)
  • lpsh+ltrim=Capped Collection(无限汇合)
  • lpush+brpop=Message Queue(音讯队列)

set 汇合和 zset 有序汇合

Redis 的汇合相当于 Java 语言外面的 HashSet 和 JS 外面的 Set,它外部的键值对是无序的惟一的。Set 汇合中最初一个 value 被移除后,数据结构主动删除,内存被回收。zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结构。它相似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保障了外部 value 的唯一性,另一方面它能够给每个 value 赋予一个 score,代表这个 value 的排序权重。它的外部实现用的是一种叫着「跳跃列表」(前面会简略介绍)的数据结构。

罕用汇合命令
  • sadd key element [element …]:增加元素,返回后果为增加胜利的元素个数
  • srem key element [element …]:删除元素,返回后果为胜利删除元素个数
  • smembers key:获取所有元素
  • sismember key element:判断元素是否在汇合中,如果给定元素 element 在汇合内返回 1,反之返回 0
  • scard key:计算元素个数,scard 的工夫复杂度为 O(1),它不会遍历汇合所有元素
  • spop key:从汇合随机弹出元素,从 3.2 版本开始,spop 也反对 [count] 参数。
  • srandmember key [count]:随机从汇合返回指定个数元素,[count]是可选参数,如果不写默认为 1
  • sinter key [key …]:求多个汇合的交加
  • suinon key [key …]:求多个汇合的并集
  • sdiff key [key …]:求多个汇合的差集
汇合简略应用截图

罕用有序汇合命令
  • zadd key score member [score member …]:增加成员,返回后果代表胜利增加成员的个数。Redis3.2 为 zadd 命令增加了 nx、xx、ch、incr 四个选项:
    • nx:member 必须不存在,才能够设置胜利,用于增加
    • xx:member 必须存在,才能够设置胜利,用于更新
    • ch: 返回此次操作后,有序汇合元素和分数发生变化的个数
    • incr: 对 score 做减少,相当于前面介绍的 zincrby
  • zcard key:计算成员个数
  • zscore key member:计算某个成员的分数
  • zrank key member 和 zrevrank key member:计算成员的排名,zrank 是从分数从低到高返回排名,zrevrank 反之
  • zrem key member [member …]:删除成员
  • zincrby key increment member:减少成员的分数
  • zrange key start end [withscores] 和 zrevrange key start end [withscores]:返回指定排名范畴的成员,zrange 是从低到高返回,zrevrange 反之。
  • zrangebyscore key min max [withscores] [limit offset count] 和 zrevrangebyscore key max min [withscores] [limit offset count] 返回指定分数范畴的成员,其中 zrangebyscore 依照分数从低到高返回,zrevrangebyscore 反之
  • zcount key min max:返回指定分数范畴成员个数
  • zremrangebyrank key start end:删除指定排名内的升序元素
  • zremrangebyscore key min max:删除指定分数范畴的成员
  • zinterstore 和 zunionstore 命令求汇合的交加和并集,可用参数比拟多,可用到再查文档

有序汇合相比汇合提供了排序字段,然而也产生了代价,zadd 的工夫 复杂度为 O(log(n)),sadd 的工夫复杂度为 O(1)。

有序汇合简略应用截图

汇合和有序汇合应用场景
  1. 给用户增加标签
  2. 给标签增加用户
  3. 依据某个权重进行排序的队列的场景,比方游戏积分排行榜,设置优先级的工作列表,学生成绩表等
对于跳跃列表

跳跃列表就是一种层级制,最上面一层所有的元素都会串起来。而后每隔几个元素挑选出一个代表来,再将这几个代表应用另外一级指针串起来。而后在这些代表里再挑出二级代表,再串起来。最终就造成了金字塔构造,如图:更多能够看:https://www.jianshu.com/p/09c…

列表、汇合和有序汇合异同

三、小性能大用处

慢查问剖析

许多存储系统(例如 MySQL)提供慢查问日志帮忙开发和运维人员定位系统存在的慢操作。所谓慢查问日志就是零碎在命令执行前后计算每条命令的执行工夫,当超过预设阈值,就将这条命令的相干信息(例如:产生工夫,耗时,命令的详细信息)记录下来,Redis 也提供了相似的性能。这里能够顺带理解一下 Redis 客户端执行一条命令的过程,分为如下 4 个局部:

对于慢查问性能,须要明确 3 件事:
1、预设阈值怎么设置?在 redis 配置文件中批改配置‘slowlog-log-slower-than’的值,单位是奥妙(1 秒 = 1000 毫秒 = 1000000 微秒),默认是 10000 微秒,如果把 slowlog-log-slower-than 设置为 0,将会记录所有命令到日志中。如果把 slowlog-log-slower-than 设置小于 0,将会不记录任何命令到日志中。2、慢查问记录寄存在哪?在 redis 配置文件中批改配置‘slowlog-max-len’的值。slowlog-max-len 的作用是指定慢查问日志最多存储的条数。实际上,Redis 应用了一个列表存加快查问日志,slowlog-max-len 就是这个列表的最大长度。当一个新的命令满足满足慢查问条件时,被插入这个列表中。当慢查问日志列表曾经达到最大长度时,最早插入的那条命令将被从列表中移出。比方,slowlog-max-len 被设置为 10,当有第 11 条命令插入时,在列表中的第 1 条命令先被移出,而后再把第 11 条命令放入列表。记录慢查问指 Redis 会对长命令进行截断,不会大量占用大量内存。在理论的生产环境中,为了减迟缓查问被移出的可能和更不便地定位慢查问,倡议将慢查问日志的长度调整的大一些。比方能够设置为 1000 以上。除了去配置文件中批改,也能够通过 config set 命令动静批改配置

> config set slowlog-log-slower-than 1000
OK
> config set slowlog-max-len 1200
OK
> config rewrite
OK

3、如何获取慢查问日志?能够应用 slowlog get 命令获取慢查问日志,在 slowlog get 前面还能够加一个数字,用于指定获取慢查问日志的条数,比方,获取 2 条慢查问日志:

> slowlog get 3
1) 1) (integer) 6107
   2) (integer) 1616398930
   3) (integer) 3109
   4) 1) "config"
      2) "rewrite"
2) 1) (integer) 6106
   2) (integer) 1613701788
   3) (integer) 36004
   4) 1) "flushall"

能够看出每一条慢查问日志都有 4 个属性组成:

  1. 惟一标识 ID
  2. 命令执行的工夫戳
  3. 命令执行时长
  4. 执行的命名和参数

此外,能够通过 slowlog len 命令获取慢查问日志的长度;通过 slowlog reset 命令清理慢查问日志。

Pipeline(流水线)机制

Redis 提供了批量操作命令(例如 mget、mset 等),无效地节约 RTT。但大部分命令是不反对批量操作的,例如要执行 n 次 hgetall 命令,并没有 mhgetall 命令存在,须要耗费 n 次 RTT。Redis 的客户端和服务端可能部署在不同的机器上。例如客户端在北京,Redis 服务端在上海,两地直线间隔约为 1300 公里,那么 1 次 RTT 工夫 = 1300×2/(300000×2/3) = 13 毫秒(光在真空中 传输速度为每秒 30 万公里,这里假如光纤为光速的 2/3),那么客户端在 1 秒 内大概只能执行 80 次左右的命令,这个和 Redis 的高并发高吞吐个性南辕北辙。Pipeline(流水线)机制能改善下面这类问题,它能将一组 Redis 命令进 行组装,通过一次 RTT 传输给 Redis,再将这组 Redis 命令的执行后果按程序返回给客户端。不应用 Pipeline 的命令执行流程:

应用 Pipeline 的命令执行流程:

Redis 的流水线是一种通信协议,没有方法通过客户端演示给大家,这里以 Jedis 为例,通过 Java API 或者应用 Spring 操作它(代码来源于互联网):

/**
 * 测试 Redis 流水线 
 * @author liu
 */
publicclass TestPipelined {
    
    /**
     * 应用 Java API 测试流水线的性能
     */
    @SuppressWarnings({"unused", "resource"})
    @Test
    public void testPipelinedByJavaAPI() {JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(20);
        jedisPoolConfig.setMaxTotal(10);
        jedisPoolConfig.setMaxWaitMillis(20000);
        
        JedisPool jedisPool = new JedisPool(jedisPoolConfig,"localhost",6379);
        Jedis jedis = jedisPool.getResource();
        long start = System.currentTimeMillis();
        // 开启流水线
        Pipeline pipeline = jedis.pipelined();
        // 测试 10w 条数据读写
        for(int i = 0; i < 100000; i++) {
            int j = i + 1;
            pipeline.set("key" + j, "value" + j);
            pipeline.get("key" + j);
        }
        // 只执行同步但不返回后果
        //pipeline.sync();
        // 以 list 的模式返回执行过的命令的后果
        List<Object> result = pipeline.syncAndReturnAll();
        long end = System.currentTimeMillis();
        // 计算耗时
        System.out.println("耗时" + (end - start) + "毫秒");
    }
    
    /**
     * 应用 RedisTemplate 测试流水线
     */
    @SuppressWarnings({"resource", "rawtypes", "unchecked", "unused"})
    @Test
    public void testPipelineBySpring() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        RedisTemplate rt = (RedisTemplate)applicationContext.getBean("redisTemplate");
        SessionCallback callback = (SessionCallback)(RedisOperations ops)->{for(int i = 0; i < 100000; i++) {
                int j = i + 1;
                ops.boundValueOps("key" + j).set("value" + j);
                ops.boundValueOps("key" + j).get();}
            returnnull;
        };
        long start = System.currentTimeMillis();
        // 执行 Redis 的流水线命令
        List result = rt.executePipelined(callback);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

网上写的测试后果为:应用 Java API 耗时在 550ms 到 700ms 之间,也就是不到 1s 就实现了 10 万次读写,应用 Spring 耗时在 1100ms 到 1300ms 之间。这个与之前一条一条命令应用,1s 内就发送几十几百条(客户端和服务端间隔导致)命令的差距不是个别的大了。留神,这里只是为了测试性能而已,当你要执行很多的命令并返回后果的时候,须要思考 List 对象的大小,因为它会“吃掉”服务器上许多的内存空间,重大时会导致内存不足,引发 JVM 溢出异样,所以在工作环境中,是须要读者本人去评估的,能够思考应用迭代的形式去解决。

事务与 Lua

multi 和 exec 命令

很多状况下咱们须要一次执行不止一个命令,而且须要其同时胜利或者失败。为了保障多条命令组合的原子性,Redis 提供了简略的事务性能以及集成 Lua 脚本来解决这个问题。Redis 提供了简略的事务性能,将一组须要一起执行的命令放到 multi 和 exec 两个命令之间。Multi 命令代表事务开始,exec 命令代表事务完结,它们之间的命令是原子程序执行的。应用案例:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> SET msg "hello chrootliu"
QUEUED
127.0.0.1:6379> GET msg
QUEUED
127.0.0.1:6379> EXEC
1) OK
1) hello chrootliu

Redis 提供了简略的事务,之所以说它简略,次要是因为它不反对事务中的回滚个性,同时无奈实现命令之间的逻辑关系计算,次要有以下几点:

  1. 不够满足原子性。一个事务执行过程中,其余事务或 client 是能够对相应的 key 进行批改的(并发状况下,例如电商常见的超卖问题),想要防止这样的并发性问题就须要应用 WATCH 命令,然而通常来说,必须通过认真思考能力决定到底须要对哪些 key 进行 WATCH 加锁。然而,额定的 WATCH 会减少事务失败的可能,而短少必要的 WATCH 又会让咱们的程序产生竞争条件。
  2. 后执行的命令无奈依赖先执行命令的后果。因为事务中的所有命令都是相互独立的,在遇到 exec 命令之前并没有真正的执行,所以咱们无奈在事务中的命令中应用后面命令的查问后果。咱们惟一能够做的就是通过 watch 保障在咱们进行批改时,如果其它事务刚好进行了批改,则咱们的批改进行,而后应用层做相应的解决。
  3. 事务中的每条命令都会与 Redis 服务器进行网络交互。Redis 事务开启之后,每执行一个操作返回的都是 queued,这里就波及到客户端与服务器端的屡次交互,明明是须要一次批量执行的 n 条命令,还须要通过屡次网络交互,显然十分节约(这个就是为什么会有 pipeline 的起因,缩小 RTT 的工夫)。
Redis 事务缺点的解决 – Lua

Lua 是一个玲珑的脚本语言,用规范 C 编写,简直在所有操作系统战争台上都能够编译运行。一个残缺的 Lua 解释器不过 200k,在目前所有脚本引擎中,Lua 的速度是最快的,这所有都决定了 Lua 是作为嵌入式脚本的最佳抉择。Redis 2.6 版本之后内嵌了一个 Lua 解释器,能够用于一些简略的事务与逻辑运算,也可帮忙开发者定制本人的 Redis 命令(例如:一次性的执行简单的操作,和带有逻辑判断的操作),在这之前,必须批改源码。在 Redis 中执行 Lua 脚本有两种办法:eval  和 evalsha,这里以 eval 做为案例介绍:eval 语法:

eval script numkeys key [key ...] arg [arg ...]

其中:

  • script    一段 Lua 脚本或 Lua 脚本文件所在门路及文件名
  • numkeys   Lua 脚本对应参数数量
  • key [key …]   Lua 中通过全局变量 KEYS 数组存储的传入参数
  • arg [arg …]   Lua 中通过全局变量 ARGV 数组存储的传入附加参数
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

Lua 执行流程图:
SCRIPT LOAD 与 EVALSHA 命令
对于不立刻执行的 Lua 脚本,或须要重用的 Lua 脚本,能够通过 SCRIPT LOAD 提前载入 Lua 脚本,这个命令会立刻返回对应的 SHA1 校验码当须要执行函数时,通过 EVALSHA 调用 SCRIPT LOAD 返回的 SHA1 即可

SCRIPT LOAD "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
"232fd51614574cf0867b83d384a5e898cfd24e5a"

EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

通过 Lua 脚本执行 Redis 命令 在 Lua 脚本中,只有应用 redis.call() 或 redis.pcall() 传入 Redis 命令就能够间接执行:

eval "return redis.call('set',KEYS[1],'bar')" 1 foo     -- 等同于在服务端执行 set foo bar

案例,应用 Lua 脚本实现拜访频率限度:

--
-- KEYS[1] 要限度的 ip
-- ARGV[1] 限度的拜访次数
-- ARGV[2] 限度的工夫
--

local key = "rate.limit:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]

local is_exists = redis.call("EXISTS", key)
if is_exists == 1then
    if redis.call("INCR", key) > limit then
        return0
    else
        return1
    end
else
    redis.call("SET", key, 1)
    redis.call("EXPIRE", key, expire_time)
    return1
end

应用办法,通过:

eval(file_get_contents(storage_path("limit.lua")), 3, "127.0.0.1", "3", "100");

redis 的事务与 Lua,就先介绍到这里了,更多的用法大家请查看 Lua 官网文档

Bitmaps

许多开发语言都提供了操作位的性能,正当地应用位可能无效地进步内存使用率和开发效率。Redis 提供了 Bitmaps 这个“数据结构”能够实现对位的操作。把数据结构加上引号次要因为:

  • Bitmaps 自身不是一种数据结构,实际上它就是字符串,然而它能够对字符串的位进行操作。
  • Bitmaps 独自提供了一套命令,所以在 Redis 中应用 Bitmaps 和应用字符串的办法不太雷同。能够把 Bitmaps 设想成一个以位为单位的数组,数组的每个单元只能存储 0 和 1,数组的下标在 Bitmaps 中叫做偏移量。

在咱们平时开发过程中,会有一些 bool 型数据须要存取,比方用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果应用一般的 key/value,每个用户要记录 365 个,当用户上亿的时候,须要的存储空间是惊人的。为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就能够齐全包容下,这就大大节约了存储空间。语法:

setbit key offset value  # 设置或者清空 key 的 value(字符串)在 offset 处的 bit 值
getbit key offset  # 返回 key 对应的 string 在 offset 处的 bit 值 
bitcount key [start end] # start end 范畴内被设置为 1 的数量,不传递 start end 默认全范畴

应用案例,统计用户登录(沉闷)状况

127.0.0.1:6379> setbit userLogin:2021-04-10 66666 1 #userId=66666 的用户登录,这是明天登录的第一个用户。(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 999999 1 #userId=999999 的用户登录,这是明天第二个登录、的用户。(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 3333 1
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 8888 1 
(integer) 0
127.0.0.1:6379> setbit userLogin:2021-04-10 100000 1
(integer) 0

127.0.0.1:6379> getbit active:2021-04-10 66666
(integer) 1
127.0.0.1:6379> getbit active:2021-04-10 55555
(integer) 

127.0.0.1:6379> bitcount active:2021-04-10
(integer) 5

因为 bit 数组的每个地位只能存储 0 或者 1 这两个状态;所以对于理论生存中,解决两个状态的业务场景就能够思考应用 bitmaps。如用户登录 / 未登录,签到 / 未签到,关注 / 未关注,打卡 / 未打卡等。同时 bitmap 还通过了相干的统计办法进行疾速统计。

HyperLogLog

HyperLogLog 并不是一种新的数据结构(理论类型为字符串类型),而 是一种基数算法,通过 HyperLogLog 能够利用极小的内存空间实现独立总数的统计,数据集能够是 IP、Email、ID 等。HyperLogLog 提供了 3 个命令:pfadd、pfcount、pfmerge。

# 用于向 HyperLogLog 增加元素
# 如果 HyperLogLog 预计的近似基数在 PFADD 命令执行之后呈现了变动,那么命令返回 1,否则返回 0 
# 如果命令执行时给定的键不存在,那么程序将先创立一个空的 HyperLogLog 构造,而后再执行命令
pfadd key value1 [value2 value3]

# PFCOUNT 命令会给出 HyperLogLog 蕴含的近似基数
# 在计算出基数后,PFCOUNT 会将值存储在 HyperLogLog 中进行缓存,晓得下次 PFADD 执行胜利前,就都不须要再次进行基数的计算。pfcount key

# PFMERGE 将多个 HyperLogLog 合并为一个 HyperLogLog,合并后的 HyperLogLog 的基数靠近于所有输出 HyperLogLog 的并集基数。pfmerge destkey key1 key2 [...keyn]
127.0.0.1:6379> pfadd totaluv user1
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 1
127.0.0.1:6379> pfadd totaluv user2
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 2
127.0.0.1:6379> pfadd totaluv user3
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 3
127.0.0.1:6379> pfadd totaluv user4
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 4
127.0.0.1:6379> pfadd totaluv user5
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 5
127.0.0.1:6379> pfadd totaluv user6 user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount totaluv
(integer) 10

HyperLogLog 内存占用量十分小,然而存在错误率,开发者在进行数据 229 构造选型时只须要确认如下两条即可:

  1. 只为了计算独立总数,不须要获取单条数据。
  2. 能够容忍肯定误差率,毕竟 HyperLogLog 在内存的占用量上有很大的劣势。

例如:如果你负责开发保护一个大型的网站,有一天老板找产品经理要网站每个网页每天的 UV 数据,而后让你来开发这个统计模块,你会如何实现? 如果统计 PV 那十分好办,给每个网页一个独立的 Redis 计数器就能够了,这个计数器 的 key 后缀加上当天的日期。这样来一个申请,incrby 一次,最终就能够统计出所有的 PV 数据。然而 UV 不一样,它要去重,同一个用户一天之内的屡次拜访申请只能计数一次。这就 要求每一个网页申请都须要带上用户的 ID,无论是登录用户还是未登录用户都须要一个惟一 ID 来标识。你兴许曾经想到了一个简略的计划,那就是为每一个页面一个独立的 set 汇合来存储所 有当天拜访过此页面的用户 ID。当一个申请过去时,咱们应用 sadd 将用户 ID 塞进去就可 以了。通过 scard 能够取出这个汇合的大小,这个数字就是这个页面的 UV 数据。没错,这是一个非常简单的计划。然而,如果你的页面访问量十分大,比方一个爆款页面几千万的 UV,你须要一个很大 的 set 汇合来统计,这就十分节约空间。如果这样的页面很多,那所须要的存储空间是惊人 的。为这样一个去重性能就消耗这样多的存储空间,值得么? 其实老板须要的数据又不须要 太准确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解 决计划呢?Redis 提供了 HyperLogLog 数据结构就是用来解决 这种统计问题的。HyperLogLog 提供不准确的去重计数计划,尽管不准确然而也不是十分不准确,标准误差是 0.81%,这样的精确度曾经能够满足下面的 UV 统计需要了。对于下面的场景,同学们可能有疑难,我或者同样能够应用 HashMap、BitMap 和 HyperLogLog 来解决。对于这三种解决方案,这边做下比照:

  • HashMap:算法简略,统计精度高,对于大量数据倡议应用,然而对于大量的数据会占用很大内存空间;
  • BitMap:位图算法,具体内容能够参考我的这篇文章,统计精度高,尽管内存占用要比 HashMap 少,然而对于大量数据还是会占用较大内存;
  • HyperLogLog:存在肯定误差,占用内存少,稳固占用 12k 左右内存,能够统计 2^64 个元素,对于下面举例的利用场景,倡议应用。

公布订阅

Redis 提供了基于“公布 / 订阅”模式的音讯机制,此种模式下,音讯发布者和订阅者不进行间接通信,发布者客户端向指定的频道(channel)公布消 息,订阅该频道的每个客户端都能够收到该音讯:

次要对应的 Redis 命令为:

subscribe channel [channel ...] # 订阅一个或多个频道
unsubscribe channel # 退订指定频道
publish channel message # 发送音讯
psubscribe pattern # 订阅指定模式
punsubscribe pattern # 退订指定模式

应用案例:关上一个 Redis 客户端,如向 TestChanne 说一声 hello:

127.0.0.1:6379> publish TestChanne hello
(integer) 1 # 返回的是接管这条音讯的订阅者数量

这样音讯就收回去了。收回去的音讯不会被长久化,也就是有客户端订阅 TestChanne 后只能接管到后续公布到该频道的音讯,之前的就接管不到了。关上另一 Redis 个客户端,这里假如发送音讯之前就关上并且订阅了 TestChanne 频道:

127.0.0.1:6379> subscribe TestChanne # 执行下面命令客户端会进入订阅状态
Reading messages... (press Ctrl-C to quit)
1) "subscribe" // 音讯类型
2) "TestChanne" // 频道
3) "hello" // 音讯内容

咱们能够利用 Redis 公布订阅性能,实现的简略 MQ 性能,实现上下游的解耦。不过须要留神了,因为 Redis 公布的音讯不会被长久化,这就会导致新订阅的客户端将不会收到历史音讯。所以,如果以后的业务场景不能容忍这些毛病,那还是用业余 MQ 吧。

GEO

Redis3.2 版本提供了 GEO(地理信息定位)性能,反对存储地理位置信 息用来实现诸如左近地位、摇一摇这类依赖于地理位置信息的性能,对于需 要实现这些性能的开发者来说是一大福音。GEO 性能是 Redis 的另一位作者 Matt Stancliff 借鉴 NoSQL 数据库 Ardb 实现的,Ardb 的作者来自中国,它提供了优良的 GEO 性能。Redis GEO 相干的命令如下:

# 增加一个空间元素,longitude、latitude、member 别离是该地理位置的经度、纬度、成员
# 这里的成员就是指代具体的业务数据,比如说用户的 ID 等
# 须要留神的是 Redis 的纬度无效范畴不是 [-90,90] 而是[-85,85]
# 如果在增加一个空间元素时,这个元素中的 menber 曾经存在 key 中,那么 GEOADD 命令会返回 0, 相当于更新了这个 menber 的地位信息
GEOADD key longitude latitude member [longitude latitude member]
# 用于增加城市的坐标信息
geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding

# 获取地理位置信息
geopos key member [member ...]
# 获取天津的坐标
geopos cities:locations tianjin

# 获取两个坐标之间的间隔
# unit 代表单位,有 4 个单位值
  - m (meter) 代表米
  - km(kilometer)代表千米
  - mi(miles)代表英里
  - ft(ft)代表尺
geodist key member1 member2 [unit]
# 获取天津和保定之间的间隔
GEODIST cities:locations tianjin baoding km

# 获取指定地位范畴内的地理信息地位汇合,此命令能够用于实现左近的人的性能
# georadius 和 georadiusbymember 两个命令的作用是一样的,都是以一个地理位置为核心算出指定半径内的其余地理信息地位,不同的是 georadius 命令的核心地位给出了具体的经纬度,georadiusbymember 只需给出成员即可。其中 radiusm|km|ft|mi 是必须参数,指定了半径(带单位),这两个命令有很多可选参数,参数含意如下:# - withcoord:返回后果中蕴含经纬度。# - withdist:返回后果中蕴含离核心节点地位的间隔。# - withhash:返回后果中蕴含 geohash,无关 geohash 前面介绍。# - COUNT count:指定返回后果的数量。# - asc|desc:返回后果依照离核心节点的间隔做升序或者降序。# - store key:将返回后果的地理位置信息保留到指定键。# - storedist key:将返回后果离核心节点的间隔保留到指定键。georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]

georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]

# 获取 geo hash
# Redis 应用 geohash 将二维经纬度转换为一维字符串,geohash 有如下特点:# - GEO 的数据类型为 zset,Redis 将所有地理位置信息的 geohash 寄存在 zset 中。# - 字符串越长,示意的地位更准确,表 3 - 8 给出了字符串长度对应的精度,例如 geohash 长度为 9 时,精度在 2 米左右。长度和精度的对应关系,请参考:https://easyreadfs.nosdn.127.net/9F42_CKRFsfc8SUALbHKog==/8796093023252281390
# - 两个字符串越类似,它们之间的间隔越近,Redis 利用字符串前缀匹配算法实现相干的命令。# - geohash 编码和经纬度是能够互相转换的。# - Redis 正是应用有序汇合并联合 geohash 的个性实现了 GEO 的若干命令。geohash key member [member ...]

# 删除操作,GEO 没有提供删除成员的命令,然而因为 GEO 的底层实现是 zset,所以能够借用 zrem 命令实现对地理位置信息的删除。zrem key member

应用案例,例如咋部门是做直播的,那直播业务个别会有一个“左近的直播”性能,这里就能够思考用 Redis 的 GEO 技术来实现这个性能。数据操作次要有两个:一是主播开播的时候写入主播 Id 的经纬度,二是主播关播的时候删除主播 Id 元素。这样就保护了一个具备地位信息的在线主播汇合提供给线上检索。大家具体应用的时候,能够去理解一下 Redis GEO 原理,次要用到了空间索引的算法 GEOHASH 的相干常识,针对索引咱们日常所见都是一维的字符,那么如何对三维空间外面的坐标点建设索引呢,间接点就是三维变二维,二维变一维。这里就不再具体论述了。
四、Redis 客户端支流编程语言都有对应的罕用 Redis 客户端,例如:

  • java -> Jedis
  • python -> redis-py
  • node -> ioredis

具体应用语法,大家能够依据本人的须要查找对应的官网文档:Jedis 文档:https://github.com/redis/jedi… 文档:https://github.com/redis/redi… 文档:https://github.com/luin/ioredis

五、长久化、主从同步与缓存设计长久化

Redis 反对 RDB 和 AOF 两种长久化机制,长久化性能无效地防止因过程 退出造成的数据失落问题,当下次重启时利用之前长久化的文件即可实现数据恢复。

  • RDB 是一次全量备份,AOF 日志是间断的增量备份,RDB 是内存数据的二进制序列化模式,在存储上十分紧凑,而 AOF 日志记录的是内存数据批改的指令记录文本。
  • AOF 以独立日志的形式记录每次写命令,重启时再从新执行 AOF 文件中的命令达到复原数据的目标。AOF 的次要作用 是解决了数据长久化的实时性,目前曾经是 Redis 长久化的支流形式。

AOF 日志在长期的运行过程中会变的无比宏大,数据库重启时须要加载 AOF 日志进行指令重放,这个工夫就会无比漫长。所以须要定期进行 AOF 重写,给 AOF 日志进行瘦身。

RDB

咱们晓得 Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。在服务线上申请的同时,Redis 还须要进行内存 RDB,内存 RDB 要求 Redis 必须进行文件 IO 操作,可文件 IO 操作是不能应用多路复用 API。这意味着单线程同时在服务线上的申请还要进行文件 IO 操作,文件 IO 操作会重大拖垮服务器申请的性能。还有个重要的问题是为了不阻塞线上的业务,就须要边长久化边响应客户端申请。长久化的同时,内存数据结构还在扭转,比方一个大型的 hash 字典正在长久化,后果一个申请过去把它给删掉了,还没长久化完呢,这可怎么办? 那该怎么办呢? Redis 应用操作系统的多过程 COW(Copy On Write) 机制来实现 RDB 长久化,以下为 RDB 备份流程:

  1. 执行 bgsave 命令,Redis 父过程判断以后是否存在正在执行的子进 程,如 RDB/AOF 子过程,如果存在 bgsave 命令间接返回。
  2. 父过程执行 fork 操作创立子过程,fork 操作过程中父过程会阻塞,通 过 info stats 命令查看 latest_fork_usec 选项,能够获取最近一个 fork 操作的耗 时,单位为微秒。
  3. 父过程 fork 实现后,bgsave 命令返回“Background saving started”信息 并不再阻塞父过程,能够持续响应其余命令。
  4. 子过程创立 RDB 文件,依据父过程内存生成长期快照文件,实现后 对原有文件进行原子替换。执行 lastsave 命令能够获取最初一次生成 RDB 的 工夫,对应 info 统计的 rdb_last_save_time 选项。
  5. 过程发送信号给父过程示意实现,父过程更新统计信息,具体见 info Persistence 下的 rdb_* 相干选项。
AOF

AOF 日志存储的是 Redis 服务器的程序指令序列,AOF 日志只记录对内存进行批改的 指令记录。假如 AOF 日志记录了自 Redis 实例创立以来所有的批改性指令序列,那么就能够通过 对一个空的 Redis 实例程序执行所有的指令,也就是「重放」,来复原 Redis 以后实例的内 存数据结构的状态。Redis 会在收到客户端批改指令后,先进行参数校验,如果没问题,就立刻将该指令文本存储到 AOF 日志中,也就是先存到磁盘,而后再执行指令。这样即便遇到突发宕机,曾经存储到 AOF 日志的指令进行重放一下就能够复原到宕机前的状态。通过 appendfsync 参数能够 管制实时 / 秒级长久化。AOF 流程:

  1. 所有的写入命令会追加到 aof_buf(缓冲区)中。
  2. AOF 缓冲区依据对应的策略向硬盘做同步操作。
  3. 随着 AOF 文件越来越大,须要定期对 AOF 文件进行重写,达到压缩的目标。
  4. 当 Redis 服务器重启时,能够加载 AOF 文件进行数据恢复。

Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会十分耗时,导致长时间 Redis 无奈对外提供服务。所以须要对 AOF 日志瘦身。Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开拓一个子过程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化结束后再将操作期间产生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加结束后就立刻代替旧的 AOF 日志文件了,瘦身工作就实现了。AOF 瘦身重写流程:

AOF 重写能够通过 auto-aof-rewrite-min-siz e 和 auto-aof-rewrite- percentage 参数管制主动触发,也能够应用 bgrewriteaof 命令手动触发。
子过程执行期间应用 copy-on-write 机制与父过程共享内存,防止内 存耗费翻倍。AOF 重写期间还须要保护重写缓冲区,保留新的写入命令防止 数据失落。单机下部署多个实例时,为了防止出现多个子过程执行重写操作,倡议做隔离管制,防止 CPU 和 IO 资源竞争。

Redis 4.0 混合长久化

重启 Redis 时,咱们很少应用 RDB 来复原内存状态,因为会失落大量数据。咱们通常 应用 AOF 日志重放,然而重放 AOF 日志性能绝对 rdb 来说要慢很多,这样在 Redis 实 例很大的状况下,启动须要破费很长的工夫。Redis 4.0 为了解决这个问题,带来了一个新的长久化选项——混合长久化。将 RDB 文 件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自 长久化开始到长久化完结的这段时间产生的增量 AOF 日志,通常这部分 AOF 日志很小。于是在 Redis 重启的时候,能够先加载 RDB 的内容,而后再重放增量 AOF 日志就可 以齐全代替之前的 AOF 全量文件重放,重启效率因而大幅失去晋升。

主从同步—简略理解

很多企业都没有应用到 Redis 的集群,然而至多都做了主从。有了主从,当 master 挂 掉的时候,运维让从库过去接管,服务就能够持续,否则 master 须要通过数据恢复和重启的过程,这就可能会拖很长的工夫,影响线上业务的继续服务。Redis 通过主从同步性能实现主节点的多个正本。从节点可灵便地通过 slaveof 命令建设或断开同步流程。同步复制分为:全量复制和局部增量复制主从节点之间保护心跳和偏移量查看机制,保障主从节点通信失常和数据统一。Redis 为了保障高性能复制过程是异步的,写命令解决完后间接返回给客户端,不期待从节点复制实现。因而从节点数据集会有提早状况。即当应用从节点用于读写拆散时会存在数据提早、过期数据、从节点可用性等问题,须要依据本身业务提前作出躲避。留神:在运维过程中,主节点存在多个从节点或者一台机器上部署大量主节点的状况下,会有复制风暴的危险。Redis Sentinel(哨兵) 主从复制是 Redis 分布式的根底,Redis 的高可用来到了主从复制将无从进行。前面的咱们会讲到 Redis 的集群模式,集群模式都依赖于本节所讲的主从复制。不过复制性能也不是必须的,如果你将 Redis 只用来做缓存,也就无须要从库做备份,挂掉了重新启动一下就行。然而只有你应用了 Redis 的长久化 性能,就必须认真对待主从复制,它是零碎数据安全的根底保障。举例:如果主节点凌晨 3 点突发宕机怎么办? 就坐等运维从床上爬起来,而后手工进行从主切换,再告诉所有的程 序把地址通通改一遍从新上线么? 毫无疑问,这样的人工运维效率太低,事变产生时预计得 至多 1 个小时能力缓过去。Sentinel 负责继续监控主从节点的衰弱,当主节点挂掉时,主动抉择一个最优的从节点切换为主节点。客户端来连贯集群时,会首先连贯 sentinel,通过 sentinel 来查问主节点的地址,而后再去连贯主节点进行数据交互。当主节点产生故障时,客户端会从新向 sentinel 要地址,sentinel 会将最新的主节点地址通知客户端。如此应用程序将无需重启即可主动实现节点切换。如图:

音讯失落

Redis 主从采纳异步复制,意味着当主节点挂掉时,从节点可能没有收到全副的同步音讯,这部分未同步的音讯就失落了。如果主从提早特地大,那么失落的数据就可能会特地 多。Sentinel 无奈保障音讯齐全不失落,然而也尽可能保障音讯少失落。它有两个选项能够 限度主从提早过大:

  • min-slaves-to-write 1
  • min-slaves-max-lag 10

第一个参数示意主节点必须至多有一个从节点在进行失常复制,否则就进行对外写服务,丢失可用性。何为失常复制,何为异样复制? 这个就是由第二个参数管制的,它的单位是秒,示意如果 10s 没有收到从节点的反馈,就意味着从节点同步不失常,要么网络断开了,要么始终没有给反馈。

Redis 最终统一

Redis 的主从数据是异步同步的,所以分布式的 Redis 零碎并不满足「一致性」要求。当客户端在 Redis 的主节点批改了数据后,立刻返回,即便在主从网络断开的状况下,主节 点仍旧能够失常对外提供批改服务,所以 Redis 满足「可用性」。Redis 保障「最终一致性」,从节点会致力追赶主节点,最终从节点的状态会和主节点 的状态将保持一致。如果网络断开了,主从节点的数据将会呈现大量不统一,一旦网络恢 复,从节点会采纳多种策略致力追赶上落后的数据,持续尽力放弃和主节点统一。

缓存

缓存的收益与老本

收益:

  • 减速读写:CPU L1/L2/L3 Cache、浏览器缓存等。因为缓存通常都是全内存的(例如 Redis、Memcache),而 存储层通常读写性能不够强悍(例如 MySQL),通过缓存的应用能够无效 地减速读写,优化用户体验。
  • 升高后端负载:帮忙后端缩小访问量和简单计算,在很大水平升高了后端的负载。老本:
  • 数据不统一:缓存层和数据层有工夫窗口不统一,和更新策略无关。
  • 代码保护老本:退出缓存后,须要同时解决缓存层和存储层的逻辑,增大了开发者保护代码的老本。
  • 运维老本:以 Redis Cluster 为例,退出后无形中减少了运维老本。应用场景:
  • 升高后端负载:对高耗费的 SQL:join 后果集 / 分组统计后果缓存。
  • 减速申请响应:利用 Redis/Memcache 优化 IO 响应工夫。
  • 大量写合并为批量写:比方计数器先 Redis 累加再批量写入 DB。
缓存更新策略—算法剔除
  • LRU:Least Recently Used,最近起码应用。
  • LFU:Least Frequently Used,最不常常应用。
  • FIFO:First In First Out,先进先出。

应用场景:剔除算法通常用于缓存使用量超过了预设的最大值时候,如何对现有的数据进行剔除。例如 Redis 应用 maxmemory-policy 这个配置作为内存最大值后对于数据的剔除策略。一致性:要清理哪些数据是由具体算法决定,开发人员只能决定应用哪种算法,所以数据的一致性是最差的。保护老本:算法不须要开发人员本人来实现,通常只须要配置最大 maxmemory 和对应的策略即可。

缓存更新策略—超时剔除

应用场景:超时剔除通过给缓存数据设置过期工夫,让其在过期工夫后主动删除,例如 Redis 提供的 expire 命令。如果业务能够容忍一段时间内,缓存层数据和存储层数据不统一,那么能够为其设置过期工夫。在数据过期后,再从实在数据源获取数据,从新放到缓存并设置过期工夫。一致性:一段时间窗口内(取决于过期工夫长短)存在一致性问题,即缓存数据和实在数据源的数据不统一。保护老本:保护老本不是很高,只需设置 expire 过期工夫即可,当然前提是利用方容许这段时间可能产生的数据不统一。

缓存更新策略—被动更新

应用场景:利用方对于数据的一致性要求高,须要在实在数据更新后,立刻更新缓存数据。例如能够利用音讯零碎或者其余形式告诉缓存更新。一致性:一致性最高,但如果被动更新产生了问题,那么这条数据很可能很长时间不会更新,所以倡议联合超时剔除一起应用成果会更好。保护老本:保护老本会比拟高,开发者须要本人来实现更新,并保障更新操作的正确性。

缓存更新策略—总结

低一致性业务:倡议配置最大内存和淘汰策略的形式应用。
高一致性业务:能够联合应用超时剔除和被动更新,这样即便被动更新出了问题,也能保证数据过期工夫后删除脏数据。

缓存可能会遇到的问题

缓存穿透:指查问一个肯定不存在的数据,因为缓存是不命中时被动写的,并且出于容错思考,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次申请都要到存储层去查问,失去了缓存的意义。在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻打咱们的利用,这就是破绽。解决办法:

  • 布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个肯定不存在的数据会被 这个 bitmap 拦挡掉,从而防止了对底层存储系统的查问压力。
  • 另外也有一个更为简略粗犷的办法(咱们采纳的就是这种),如果一个查问返回的数据为空(不论是数 据不存在,还是系统故障),咱们依然把这个空后果进行缓存,但它的过期工夫会很短,最长不超过五分钟。

缓存雪崩:指在咱们设置缓存时采纳了雷同的过期工夫,导致缓存在某一时刻同时生效,申请全副转发到 DB,DB 刹时压力过重雪崩。解决办法:咱们能够在原有的生效工夫根底上减少一个随机值,比方 1 - 5 分钟随机,这样每一个缓存的过期工夫的反复率就会升高,就很难引发个体生效的事件。缓存击穿:对于一些设置了过期工夫的 key,如果这些 key 可能会在某些工夫点被超高并发地拜访,是一种十分“热点”的数据。这个时候,须要思考一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,前者则是很多 key。缓存在某个工夫点过期的时候,恰好在这个工夫点对这个 Key 有大量的并发申请过去,这些申请发现缓存过期个别都会从后端 DB 加载数据并回设到缓存,这个时候大并发的申请可能会霎时把后端 DB 压垮。解决办法:互斥锁、永远不过期设置、资源爱护等等。缓存无底洞问题:Facebook 的工作人员反馈 2010 年已达到 3000 个 memcached 节点,贮存数千 G 的缓存。他们发现一个问题– memcached 的连贯效率降落了,于是增加 memcached 节点,增加完之后,并没有恶化。称为“无底洞”景象。起因:客户端一次批量操作会波及屡次网络操作,也就意味着批量操作会随着实例的增多,耗时会一直增大。服务端网络连接次数变多,对实例的性能也有肯定影响。即:更多的机器不代表更多的性能,所谓“无底洞”就是说投入越多不肯定产出越多。解决方案有:串行 mget、串行 IO、并行 IO、Hash tag 实现等,更多请看:缓存无底洞问题(http://ifeve.com/redis-multig…)

六、常识拓展

缓存与数据库同步策略 (如何保障缓存(Redis) 与数据库 (MySQL) 的一致性?)

对于热点数据(常常被查问,但不常常被批改的数据),咱们个别会将其放入 Redis 缓存中,以减少查问效率,但须要保障从 Redis 中读取的数据与数据库中存储的数据最终是统一的,这就是经典的 缓存与数据库同步问题 。那么,如何保障缓存(Redis) 与数据库 (MySQL) 的一致性呢?依据缓存是删除还是更新,以及操作程序大略是能够分为上面四种状况:

  1. 先更新数据库,再更新缓存
  2. 先更新缓存,再更新数据库
  3. 先删除缓存,再更新数据库
  4. 先更新数据库,再删除缓存
删除缓存比照更新缓存
  • 删除缓存: 数据只会写入数据库,不会写入缓存,只会删除缓存
  • 更新缓存: 数据岂但写入数据库,还会写入缓存

删除缓存

  • 长处:操作简略,无论更新操作是否简单,间接删除,并且能避免更新呈现的线程平安问题
  • 毛病:删除后,下一次查问无奈在 cache 中查到,会有一次 Cache Miss,这时须要从新读取数据库,高并发下可能会呈现下面说的缓存问题

更新缓存

  • 长处:命中率高,间接更新缓存,不会有 Cache Miss 的状况
  • 毛病:更新缓存耗费较大,尤其在简单的操作流程中

那到底是抉择更新缓存还是删除缓存呢,次要取决于更新缓存的复杂度

  • 更新缓存的代价很小,此时咱们应该更偏向于更新缓存,以保障更高的缓存命中率
  • 更新缓存的代价很大,此时咱们应该更偏向于删除缓存

例如:只是简略的更新一下用户积分,只操作一个字段,那就能够采纳更新缓存,还有相似秒杀下商品库存数量这种并发下查问频繁的数据,也能够应用更新缓存,不过也要留神线程平安的问题,避免产生脏数据。然而当更新操作的逻辑较简单时,须要波及到其它数据,如用户购买商品付款时,须要思考打折、优惠券、红包等多种因素,这样须要缓存与数据库进行屡次交互,将打折等信息传入缓存,再与缓存中的其它值进行计算能力失去最终后果,此时更新缓存的耗费要大于间接淘汰缓存。所以还是要依据业务场景来进行抉择,不过大部分场景下删除缓存操作简略,并且带来的副作用只是减少了一次 Cache Miss,倡议作为通用的解决形式。

先更新数据库,再更新缓存

这种形式就适宜更新缓存的代价很小的数据,例如下面说的用户积分,库存数量这类数据,同样还是要留神线程平安的问题。
线程平安角度 同时有申请 A 和申请 B 进行更新操作,那么会呈现

  1. 线程 A 更新了数据库
  2. 线程 B 更新了数据库
  3. 线程 B 更新了缓存
  4. 线程 A 更新了缓存

这就呈现申请 A 更新缓存应该比申请 B 更新缓存早才对,然而因为网络等起因,B 却比 A 更早更新了缓存,这就导致了脏数据。
业务场景角度 有如下两种不适宜场景:

  1. 如果你是一个写数据库场景比拟多,而读数据场景比拟少的业务需要,采纳这种计划就会导致,数据压根还没读到,缓存就被频繁的更新,节约性能
  2. 如果你写入数据库的值,并不是间接写入缓存的,而是要通过一系列简单的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是也节约性能的
先更新缓存,再更新数据库

这种状况应该是和第一种状况一样会存在线程平安问题的,然而这种状况是有人应用过的,依据书籍《淘宝技术这十年》里,多隆把商品详情页放入缓存,采取的正是先更新缓存,再将缓存中的数据异步更新到数据库这种形式,有趣味理解的能够查看这篇博客: https://www.cnblogs.com/rjzhe… 还有当初互联网常见的点赞性能,也能够采纳这种形式,有趣味理解的能够查看这篇文章: https://juejin.im/post/5bdc25…

先删除缓存,再更新数据库

简略的想一下,如同这种形式不错,就算是第一步删除缓存胜利,第二步写数据库失败,则只会引发一次 Cache Miss,对数据没有影响,其实认真一想并发下也很容易导致了脏数据,例如

  1. 申请 A 进行写操作,删除缓存
  2. 申请 B 查问发现缓存不存在
  3. 申请 B 去数据库查问失去旧值
  4. 申请 B 将旧值写入缓存
  5. 申请 A 将新值写入数据库

那怎么解决呢,先看第四种状况(先更新数据库,再删除缓存),前面再对立说第三种和第四种的解决方案。

先更新数据库,再删除缓存

先说一下,国外有人提出了一个缓存更新套路,名为 Cache-Aside Pattern:https://docs.microsoft.com/en…

  • 生效:应用程序先从 cache 取数据,没有失去,则从数据库中取数据,胜利后,放到缓存中
  • 命中:应用程序从 cache 中取数据,渠道后返回
  • 更新:先把数据存到数据库中,胜利后再让缓存生效

更新操作就是先更新数据库,再删除缓存;读取操作先从缓存取数据,没有,则从数据库中取数据,胜利后,放到缓存中;这是规范的设计方案,包含 Facebook 的论文 Scaling Memcache at Facebook:chrome-extension://ikhdkkncnoglghljlkmcimlnlhkeamad/pdf-viewer/web/viewer.html?file=https%3A%2F%2Fwww.usenix.org%2Fsystem%2Ffiles%2Fconference%2Fnsdi13%2Fnsdi13-final170_update.pdf 也应用了这个策略。为什么他们都用这种形式呢,这种状况不存在并发问题么? 答案是也存在,然而呈现概率比第三种低,例如:

  1. 申请缓存刚好生效
  2. 申请 A 查询数据库,得一个旧值
  3. 申请 B 将新值写入数据库
  4. 申请 B 删除缓存
  5. 申请 A 将查到的旧值写入缓存

这样就呈现脏数据了,然而,实际上呈现的概率可能非常低,因为这个条件须要产生在读缓存时缓存生效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必须在写操作前进入数据库操作,而又要晚于写操作删除缓存,所有的这些条件都具备的概率根本并不大,然而还是会有呈现的概率。并且如果第一步写数据库胜利,第二步删除缓存失败,这样也导致脏数据,请看解决方案。

计划三四脏数据解决方案

那怎么解决呢,能够采纳 延时双删策略(缓存双淘汰法),能够将后面所造成的缓存脏数据,再次删除

  1. 先删除 (淘汰) 缓存
  2. 再写数据库(这两步和原来一样)
  3. 休眠 1 秒,再次删除 (淘汰) 缓存

或者是

  1. 先写数据库
  2. 再删除 (淘汰) 缓存(这两步和原来一样)
  3. 休眠 1 秒,再次删除 (淘汰) 缓存

这个 1 秒应该看你的业务场景,应该自行评估本人的我的项目的读数据业务逻辑的耗时,而后写数据的休眠工夫则在读数据业务逻辑的耗时根底上,加几百毫秒即可,这么做确保读申请完结,写申请能够删除读申请造成的缓存脏数据。如果你用了 MySql 的读写拆散架构怎么办?,例如:

  1. 申请 A 进行写操作,删除缓存
  2. 申请 A 将数据写入数据库了,(或者是先更新数据库,后删除缓存)
  3. 申请 B 查问缓存发现,缓存没有值
  4. 申请 B 去从库查问,这时,还没有实现主从同步,因而查问到的是旧值
  5. 申请 B 将旧值写入缓存
  6. 数据库实现主从同步,从库变为新值

这种情景,就是数据不统一的起因,还是采纳延时双删策略 (缓存双淘汰法),只是,休眠工夫批改为在主从同步的延时工夫根底上,加几百毫秒 并且为了性能更快,能够把第二次删除缓存能够做成异步的,这样不会阻塞申请了,如果再谨严点,避免第二次删除缓存失败,这个异步删除缓存能够加上重试机制,失败始终重试,直到胜利。 这里给出两种重试机制参考计划一

  1. 更新数据库数据
  2. 缓存因为种种问题删除失败
  3. 将须要删除的 key 发送至音讯队列
  4. 本人生产音讯,取得须要删除的 key
  5. 持续重试删除操作,直到胜利

然而,该计划有一个毛病,对业务线代码造成大量的侵入,于是有了计划二,启动一个订阅程序去订阅数据库的 Binlog,取得须要操作的数据。在应用程序中,另起一段程序,取得这个订阅程序传来的信息,进行删除缓存操作
计划二

  1. 更新数据库数据
  2. 数据库会将操作信息写入 binlog 日志当中
  3. 订阅程序提取出所须要的数据以及 key
  4. 另起一段非业务代码,取得该信息
  5. 尝试删除缓存操作,发现删除失败
  6. 将这些信息发送至音讯队列
  7. 从新从音讯队列中取得该数据,重试操作

上述的订阅 Binlog 程序在 MySql 中有现成的中间件叫 Canal,能够实现订阅 Binlog 日志的性能,另外,重试机制,这里采纳的是音讯队列的形式。如果对一致性要求不是很高,间接在程序中另起一个线程,每隔一段时间去重试即可,这些大家能够灵便自由发挥,只是提供一个思路。
总结:大部分应该应用的都是第三种或第四种形式,如果都是采纳延时双删策略(缓存双淘汰法),可能区别不会很大,不过第四种形式呈现脏数据概率是更小点,更多的话还是要联合本身业务场景应用,灵便变通。

分布式锁

例如一个操作要批改用户的状态,批改状态须要先读出用户的状态,在内存里进行修 改,改完了再存回去。如果这样的操作同时进行了,就会呈现并发问题,因为读取和保留状 态这两个操作不是原子的。(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就始终运行到完结,两头不会有任何 context switch 线程切换。)如图:

 

这个时候就要应用到分布式锁来限度程序的并发执行。
分布式锁实质上要实现的指标就是在 Redis 外面占一个“茅坑”,当别的过程也要来占 时,发现曾经有人蹲在那里了,就只好放弃或者稍后再试。占坑个别是应用 setnx(set if not exists) 指令,只容许被一个客户端占坑。先来先占,用 完了,再调用 del 指令开释茅坑。

setnx lock:codehole true
OK
 ... do something critical ... 
del lock:codehole
(integer) 1

然而有个问题,如果逻辑执行到两头出现异常了,可能会导致 del 指令没有被调用,这样 就会陷入死锁,锁永远得不到开释。于是咱们在拿到锁之后,再给锁加上一个过期工夫,比方 5s,这样即便两头出现异常也 能够保障 5 秒之后锁会主动开释。

setnx lock:codehole true 
OK 
> expire lock:codehole 5 ... 
do something critical ... 
> del lock:codehole
 (integer) 1

如果在 setnx 和 expire 之间服务器过程忽然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。这种问题的本源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可 以一起执行就不会呈现问题。兴许你会想到用 Redis 事务来解决。然而这里不行,因为 expire 是依赖于 setnx 的执行后果的,如果 setnx 没抢到锁,expire 是不应该执行的。事务里没有 if else 分支逻辑,事务的特点是一口气执行,要么全副执行要么一个都不执行。Redis 2.8 版本中作者退出了 set 指令的扩大参数,使得 setnx 和 expire 指令能够一起执行:

set lock:codehole trueex 5 nx 
OK 
... do something critical ... 
del lock:codehole

下面这个指令就是 setnx 和 expire 组合在一起的原子指令,它就是分布式锁的奥义所在。

分布式锁存在的问题

超时问题:如果在加锁和开释锁之间的逻辑执行的太长,以至于超出了锁的超时限度,就会呈现问题。因为这时候锁过期了,第二个线程从新持有了这把锁,然而紧接着第一个线程执行完了业务逻辑,就把锁给开释了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。单节点的分布式锁问题:在单 Matste 的主从 Matster-Slave Redis 零碎中,失常状况下 Client 向 Master 获取锁之后同步给 Slave,如果 Client 获取锁胜利之后 Master 节点挂掉,并且未将该锁同步到 Slave,之后在 Sentinel 的帮忙下 Slave 降级为 Master 然而并没有之前未同步的锁的信息,此时如果有新的 Client 要在新 Master 获取锁,那么将可能呈现两个 Client 持有同一把锁的问题,来看个图来想下这个过程:

所以,为了保障本人的锁只能本人开释须要减少唯一性的校验,综上基于单 Redis 节点的获取锁和开释锁的简略过程如下:

// 获取锁 unique_value 作为唯一性的校验
SET resource_name unique_value NX PX 30000

// 开释锁 比拟 unique_value 是否相等 防止误开释
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
对于分布式锁的 Redlock 算法

Redis 性能好并且实现不便,然而单节点的分布式锁在故障迁徙时产生平安问题,Redlock 算法是 Redis 的作者 Antirez 提出的集群模式分布式锁,基于 N 个齐全独立的 Redis 节点实现分布式锁的高可用。在 Redis 的分布式环境中,咱们假如有 N 个齐全相互独立的 Redis 节点,在 N 个 Redis 实例上应用与在 Redis 单实例下雷同办法获取锁和开释锁。当初假如有 5 个 Redis 主节点(大于 3 的奇数个),这样根本保障他们不会同时都宕掉,获取锁和开释锁的过程中,客户端会执行以下操作:

  1. 获取以后 Unix 工夫,以毫秒为单位
  2. 顺次尝试从 5 个实例,应用雷同的 key 和具备唯一性的 value 获取锁 当向 Redis 申请获取锁时,客户端应该设置一个网络连接和响应超时工夫,这个超时工夫应该小于锁的生效工夫,这样能够防止客户端死等
  3. 客户端应用以后工夫减去开始获取锁工夫就失去获取锁应用的工夫。当且仅当从半数以上的 Redis 节点取到锁,并且应用的工夫小于锁生效工夫时,锁才算获取胜利
  4. 如果取到了锁,key 的真正无效工夫等于无效工夫减去获取锁所应用的工夫,这个很重要
  5. 如果因为某些起因,获取锁失败(没有在半数以上实例取到锁或者取锁工夫曾经超过了无效工夫),客户端应该在所有的 Redis 实例上进行解锁,无论 Redis 实例是否加锁胜利,因为可能服务端响应音讯失落了然而理论胜利了,毕竟多开释一次也不会有问题

对于集群

在大数据高并发场景下,单个 Redis 实例往往会显得顾此失彼。首先体现在内存上,单个 Redis 的内存不宜过大,内存太大会导致 rdb 文件过大,进一步导致主从同步时全量同步工夫过长,在实例重启复原时也会耗费很长的数据加载工夫,特地是在云环境下,单个实例内存往往都是受限的。其次体现在 CPU 的利用率上,单个 Redis 实例只能利用单个外围,这单个外围要实现海量数据的存取和管理工作压力会十分大。所以孕育而生了 Redis 集群,集群计划次要有以下几种:Sentinel:Sentinel(哨兵)模式,基于主从复制模式,只是引入了哨兵来监控与主动解决故障 Codis:Codis 是 Redis 集群计划之一,令咱们感到自豪的是,它是中国人开发并开源的,来自前豌豆荚中间件团队。Cluster:Redis Cluster 是 Redis 的亲儿子,它是 Redis 作者本人提供的 Redis 集群化计划。

腾讯工程师技术干货中转:

  1. 快珍藏!最全 GO 语言实现设计模式【下】

  2. 如何成为优良工程师之软技能篇

  3. 如何更好地应用 Kafka?

  4. 从鹅厂实例登程!剖析 Go Channel 底层原理

正文完
 0