从应用到底层:36 张图带你进入 Redis 世界(下)

以下文章来源于 sowhat1412 ,作者 sowhat1412

从应用到底层:36张图带你进入Redis世界(下)总感觉哪里不对,但是又说不上来

4.6 事务

MySQL 中的事务还是挺多道道的还要,而在 Redis 中的事务只要有如下三步:

从应用到底层:36张图带你进入Redis世界(下)

关于事务具体结论:

1、redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。 

2、Redis 事务没有隔离级别的概念:批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到

3、Redis不保证原子性:Redis 中单条命令是原子性执行的,但事务不保证原子性。

4、Redis 编译型错误事务中所有代码均不执行,指令使用错误。运行时异常是错误命令导致异常,其他命令可正常执行。

5、watch 指令类似于乐观锁,在事务提交时,如果 watch 监控的多个 KEY 中任何 KEY 的值已经被其他客户端更改,则使用 EXEC 执行事务时,事务队列将不会被执行。

4.7、正确开发步骤

上线前:Redis 高可用,主从 + 哨兵,Redis cluster,避免全盘崩溃。

上线时:本地 ehcache 缓存 + Hystrix 限流 + 降级,避免 MySQL 扛不住。上线后:Redis 持久化采用 RDB + AOF 来保证断点后自动从磁盘上加载数据,快速恢复缓存数据。

5、分布式锁

日常开发中我们可以用 synchronized 、Lock 实现并发编程。但是 Java 中的锁只能保证在同一个 JVM 进程内中执行。如果在分布式集群环境下用锁呢?日常一般有两种选择方案。

5.1、 Zookeeper 实现分布式锁

你需要知道一点基本 zookeeper 知识:

1、持久节点:客户端断开连接 zk 不删除 persistent 类型节点 2、临时节点:客户端断开连接 zk 删除 ephemeral 类型节点 3、顺序节点:节点后面会自动生成类似 0000001 的数字表示顺序 4、节点变化的通知:客户端注册了监听节点变化的时候,会调用回调方法

大致流程如下,其中注意每个节点只监控它前面那个节点状态,从而避免羊群效应。关于模板代码百度即可。

从应用到底层:36张图带你进入Redis世界(下)

缺点:

频繁的创建删除节点,加上注册 watch 事件,对于 zookeeper 集群的压力比较大,性能也比不上 Redis 实现的分布式锁。

5.2、 Redis 实现分布式锁

本身原理也比较简单,Redis 自身就是一个单线程处理器,具备互斥的特性,通过 setNX,exist 等命令就可以完成简单的分布式锁,处理好超时释放锁的逻辑即可。

SETNX

SETNX 是 SET if Not eXists 的简写,日常指令是 SETNX key value,如果 key 不存在则 set 成功返回 1,如果这个 key 已经存在了返回 0。

SETEX

SETEX key seconds value 表达的意思是 将值 value 关联到 key ,并将 key 的生存时间设为多少秒。如果 key 已经存在,setex 命令将覆写旧值。并且 setex 是一个原子性 (atomic) 操作。

加锁:

一般就是用一个标识唯一性的字符串比如 UUID 配合 SETNX 实现加锁。

解锁:

这里用到了 LUA 脚本,LUA 可以保证是原子性的,思路就是判断一下 Key 和入参是否相等,是的话就删除,返回成功 1,0 就是失败。

缺点:

这个锁是无法重入的,且自己实心的话各种边边角角都要考虑到,所以了解个大致思路流程即可,工程化还是用开源工具包就行

5.3、 Redisson 实现分布式锁

Redisson 是在 Redis 基础上的一个服务,采用了基于 NIO 的 Netty 框架,不仅能作为 Redis 底层驱动客户端,还能将原生的 RedisHash,List,Set,String,Geo,HyperLogLog 等数据结构封装为 Java 里大家最熟悉的映射(Map),列表(List),集(Set),通用对象桶(Object
Bucket),地理空间对象桶(Geospatial Bucket),基数估计算法(HyperLogLog)等结构。

这里我们只是用到了关于分布式锁的几个指令,他的大致底层原理:

从应用到底层:36张图带你进入Redis世界(下)

Redisson 加锁解锁 大致流程图如下:

从应用到底层:36张图带你进入Redis世界(下)

6、Redis 过期策略和内存淘汰策略

6.1、Redis 的过期策略

Redis 中 过期策略 通常有以下三种:

  1. 定时过期

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

  1. 惰性过期

只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。

  1. 定期过期

每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。

expires 字典会保存所有设置了过期时间的 key 的过期时间数据,其中 key 是指向键空间中的某个键的指针,value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。

Redis 采用的过期策略:惰性删除 + 定期删除。memcached 采用的过期策略:惰性删除。

6.2、6 种内存淘汰策略

Redis 的内存淘汰策略是指在 Redis 的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

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

2、volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

3、volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

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

5、allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 6、no-enviction(驱逐):禁止驱逐数据,不删除的意思。

面试常问常考的也就是LRU了,大家熟悉的 LinkedHashMap 中也实现了 LRU 算法的,实现如下:

min-replicas-to-write 3  表示连接到master的最少slave数量
min-replicas-max-lag 10  表示slave连接到master的最大延迟时间

6.2、总结

Redis 的内存淘汰策略的选取并不会影响过期的 key 的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据,过期策略用于处理过期的缓存数据

7、Redis 集群高可用

单机问题有机器故障、容量瓶颈、QPS 瓶颈。在实际应用中,Redis 的多机部署时候会涉及到 redis 主从复制、Sentinel 哨兵模式、Redis Cluster。

从应用到底层:36张图带你进入Redis世界(下)

7.1、redis 主从复制

该模式下 具有高可用性且读写分离, 会采用 增量同步 跟 全量同步 两种机制。

7.1.1、全量同步

从应用到底层:36张图带你进入Redis世界(下)

Redis 全量复制一般发生在Slave 初始化阶段,这时 Slave 需要将 Master 上的所有数据都复制一份:

1、slave 连接 master,发送 psync 命令。

2、master 接收到 psync 命名后,开始执行 bgsave 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令。

3、master 发送快照文件到 slave,并在发送期间继续记录被执行的写命令。4、slave 收到快照文件后丢弃所有旧数据,载入收到的快照。

5、master 快照发送完毕后开始向 slave 发送缓冲区中的写命令。

6、slave 完成对快照的载入,开始接收命令请求,并执行来自 master 缓冲区的写命令。

7.1.2、增量同步

也叫指令同步,就是从库重放在主库中进行的指令。Redis 会把指令存放在一个环形队列当中,因为内存容量有限,如果备机一直起不来,不可能把所有的内存都去存指令,也就是说,如果备机一直未同步,指令可能会被覆盖掉。

Redis 增量复制是指 Slave 初始化后开始正常工作时 master 发生的写操作同步到 slave 的过程。增量复制的过程主要是 master 每执行一个写命令就会向 slave 发送相同的写命令。

从应用到底层:36张图带你进入Redis世界(下)

7.1.3、Redis 主从同步策略:

1、主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。2、slave 在同步 master 数据时候如果 slave 丢失连接不用怕,slave 在重新连接之后丢失重补。

3、一般通过主从来实现读写分离,但是如果 master 挂掉后如何保证 Redis 的 HA 呢?引入 Sentinel 进行 master 的选择。

7.2、高可用之哨兵模式

从应用到底层:36张图带你进入Redis世界(下)

Redis-sentinel 本身是一个独立运行的进程,一般 sentinel 集群

节点数至少三个且奇数个,它能监控多个 master-slave 集群,sentinel 节点发现 master 宕机后能进行自动切换。Sentinel 可以监视任意多个主服务器以及主服务器属下的从服务器,并在被监视的主服务器下线时,自动执行故障转移操作。这里需注意 sentinel 也有 single-point-of-failure 问题。大致罗列下哨兵用途:

集群监控:循环监控 master 跟 slave 节点。

消息通知:当它发现有 redis 实例有故障的话,就会发送消息给管理员

故障转移:这里分为主观下线 (单独一个哨兵发现 master 故障了)。客观下线 (多个哨兵进行抉择发现达到 quorum 数时候开始进行切换)。

配置中心:如果发生了故障转移,它会通知将 master 的新地址写在配置中心告诉客户端。

7.3、Redis Cluster

RedisCluster 是 Redis 的分布式解决方案,在 3.0 版本后推出的方案,有效地解决了 Redis 分布式的需求。

从应用到底层:36张图带你进入Redis世界(下)

7.3.1、分区规则

从应用到底层:36张图带你进入Redis世界(下)

常见的分区规则

节点取余:hash(key) % N

一致性哈希:一致性哈希环

虚拟槽哈希:CRC16[key] & 16383

RedisCluster 采用了虚拟槽分区方式,具体的实现细节如下:

1、采用去中心化的思想,它使用虚拟槽 solt 分区覆盖到所有节点上,取数据一样的流程,节点之间使用轻量协议通信Gossip来减少带宽占用所以性能很高,

2、自动实现负载均衡与高可用,自动实现failover并且支持动态扩展,官方已经玩到可以 1000 个节点 实现的复杂度低。

3、每个 Master 也需要配置主从,并且内部也是采用哨兵模式,如果有半数节点发现某个异常节点会共同决定更改异常节点的状态。

4、如果集群中的 master 没有 slave 节点,则 master 挂掉后整个集群就会进入fail状态,因为集群的 slot 映射不完整。如果集群超过半数以上的 master 挂掉,集群都会进入 fail 状态

5、官方推荐 集群部署至少要 3 台以上的 master 节点

8、Redis 限流

经常乘坐北京西二旗地铁或者在北京西站乘坐的时候经常会遇到一种情况就是如果人很多,地铁的工作人员拿个小牌前面一档让你等会儿再检票,这就是实际生活应对人流量巨大的措施。

在开发高并发系统时,有三把利器用来保护系统:缓存、降级和限流。那么何为限流呢?顾名思义,限流就是限制流量,就像你宽带包了 1 个 G 的流量,用完了就没了。通过限流,我们可以很好地控制系统的 qps,从而达到保护系统的目的。

1、基于 Redis 的 setnx、zset

1.2、setnx

比如我们需要在 10 秒内限定 20 个请求,那么我们在 setnx 的时候可以设置过期时间 10,当请求的 setnx 数量达到 20 时候即达到了限流效果。

缺点:比如当统计 1-10 秒的时候,无法统计 2-11 秒之内,如果需要统计 N 秒内的 M 个请求,那么我们的 Redis 中需要保持 N 个 key 等等问题

1.3、zset

其实限流涉及的最主要的就是滑动窗口,上面也提到 1-10 怎么变成 2-11。其实也就是起始值和末端值都各 +1 即可。我们可以将请求打造成一个zset 数组,当每一次请求进来的时候,value 保持唯一,可以用 UUID 生成,而 score 可以用当前时间戳表示,因为 score 我们可以用来计算当前时间戳之内有多少的请求数量。而 zset 数据结构也提供了range方法让我们可以很轻易的获取到 2 个时间戳内有多少请求,

缺点:就是 zset 的数据结构会越来越大。

2、漏桶算法

漏桶算法思路:把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

从应用到底层:36张图带你进入Redis世界(下)

3、令牌桶算法

令牌桶算法的原理:可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。

从应用到底层:36张图带你进入Redis世界(下)

细节流程大致:

1、所有的请求在处理之前都需要拿到一个可用的令牌才会被处理。

2、根据限流大小,设置按照一定的速率往桶里添加令牌。

3、设置桶最大可容纳值,当桶满时新添加的令牌就被丢弃或者拒绝。

4、请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除。

5、令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。

工程化:

1、自定义注解、aop、Redis + Lua 实现限流。

2、推荐 guava 的 RateLimiter 实现。

9、常见知识点

  1. 字符串模糊查询时用 Keys 可能导致线程阻塞,尽量用 scan 指令进行无阻塞的取出数据然后去重下即可。
  2. 多个操作的情况下记得用 pipeLine 把所有的命令一次发过去,避免频繁的发送、接收带来的网络开销,提升性能。
  3. bigkeys 可以扫描 redis 中的大 key,底层是使用 scan 命令去遍历所有的键,对每个键根据其类型执行 STRLEN、LLEN、SCARD、HLEN、ZCARD 这些命令获取其长度或者元素个数。缺陷是线上试用并且个数多不一定空间大,
  4. 线上应用记得开启 Redis 慢查询日志哦,基本思路跟 MySQL 类似。
  5. Redis 中因为内存分配策略跟增删数据是会导致内存碎片,你可以重启服务也可以执行 activedefrag yes 进行内存重新整理来解决此问题。

1、Ratio >1 表明有内存碎片,越大表明越多严重。

2、Ratio < 1 表明正在使用虚拟内存,虚拟内存其实就是硬盘,性能比内存低得多,这是应该增强机器的内存以提高性能。

3、一般来说,mem_fragmentation_ratio 的数值在 1 ~ 1.5 之间是比较健康的。

10、End

关于 Redis 先吹逼这么多 (本来想写秒杀的,怕写太长,估计能看到这就算是认真阅读者了),如果你感觉没看够那得价钱。

从应用到底层:36 张图带你进入 Redis 世界(上)