https://www.infoq.cn/article/FoQGIk*BzdQWJJ0tKqrJ
Redis 集群的历史
Redis 在 3.0 前一般有两种集群方案,一是 proxy(Twemproxy、Codis),二是使用 Sentinel(哨兵)。 通过 Sentinel 是一种使用哨兵来达到高可用的方案,而 proxy 是用于在前置上进行 sharding 用代理给后端的 redis 集群的方案,达到负载均衡的方案,在单个分片的 redis 中作主从。 因为本文要重点讲解的不是 3.0 前的方案,因此说的比较粗略。
Redis3.0 提供了官方的 Redis cluster 机制支持。主要通过内部无中心的多个节点来达到集群、高可用的作用。下面是 Redis Cluster 的架构图:
Redis Cluster 的重要机制
-
sharding 由 Redis cluster 根据 client 调用 redis 的 key 进行 hash 取模得到一个 code,根据这个 code 放到 16384 个 slot 中。在以上的架构图中 slot1 组、slot2 组、slot3 组服务器中分别是对应的差不多 1/3 的 slot。这样就得到了根据 key 的 shading。
-
说到 sharding 就会说到怎么进行扩容和缩容,redis cluster 也提供了工具进行迁移。 不过由于不是一致性 hash,所以涉及到迁移数据的节点数会多于一致性 hash,但是迁移的量还是可控的,只会迁移部分以达到平均的效果。 (redis cluster 迁移详细机制需要另外详细研究)
-
在 redis cluster 的一个 slot 组中,采用的是主备的高可用模式,只有 master 对外提供服务,如果 master 挂掉,则 slave 会成为新的 master,由新的 master 提供服务,在切换的过程中会在短时间的 redis 服务不可用。
-
redis cluster 进入 fail 状态的条件:
- 如果同时有大于 N(N 为总 slot 组数)*1/2 的 master 节点数同时挂掉,则 cluster 进入 fail 状态,因为剩下的 master 已经无法选出新的 master 节点了。 分布式系统中必须要有大于 N(N 为总 slot 组数)*1/2 的节点存在才能判断有效。 节如果有是 3 主 3 备的集群,如果同时有 2 个 master 节点挂掉了,那么集群就挂掉了。
- 如果一个 slot 组中的服务器已经没有备用服务器了,这时 master 挂掉后,redis cluster 也会进入 fail 状态。 因为部分 slot 的数据已经查询不到了。
-
redis cluster 具备高可用、sharding、负载均衡的功能。
-
如果多个 key 想要人为控制落到一个 slot 组上,可以通过对 key 进行改造实施。即如果 key 都以{key_pre}idxxxx 这样,那么所有的 key 将是以 key_pre 去确定 slot 组,这样就达到了以{key_pre}开头的 key 都会是在一个 slot 组。
-
当向一个 master 中写入数据时,数据是进行迅速返回的,返回后再进行主从同步的方式向 slave 进行同步,因此这里是损失了 CAP 中的一致性的。 在将来的 redis 版本中可能会开放同步写的方式写入 slave,以维护一致性,当然这样会损失一定的写入速度。
-
在上文中提到的在做主从切换的时候,会有短时的不可用状态,因此会操作分布式理论 CAP 中可用性。
-
单个 redis 服务器上的请求是顺序执行的,因为 redis 服务器是单进程、单线程的。
分布式锁
分布式锁有很多的实现方案,通常有数据库、文件系统、zookeeper、redis。下面讲述基于 redis cluster 的分布式锁方案。
基于 redis 事务实现
严格来说这并不是分布式锁,只是通过改造可以实现锁的效果。这里并不是实现锁定其他的线程被阻塞的效果,而是如果数据被其他客户端修改了就返回失败。原理是基于 reids 的 multi 和 watch 命令。 在事务开始前对要锁到的数据进行 watch,进行业务操作后,如果发现锁定的数据已经变了,就提交失败,重新进行业务操作。 在这个方案中如果执行失败就一直反复执行直到成功,也是实现了多个 redis 客户同时修改一个数据时的协调的锁的功能。
伪代码如下:
jedis.set(
"balance", String.valueOf(balance));
| |
jedis.watch(
"balance");
| |
Transaction transaction = jedis.multi
();
| |
transaction.decr
By("balance",amtToSubtract);/
| |
transaction.incr
By("debt", amtToSubtract);
| |
List result = transaction.exec
();// 执行事务
| |
if(result == null){
| |
// 重新执行事务或者其他。
| |
}
else{
| |
// 事务执行成功。
| |
}
|
基于 setNX EX 实现
Redis set key 时的一个 NX 参数可以保证在这个 key 不存在的情况下写入成功。并且再加上 EX 参数可以让该 key 在超时之后自动删除。
jedis 伪代码:
String
set(String key, String value, String nxxx, String expx,longtime);
|
为什么要以一代代码一个命令去实现呢,是因为要防止 NX 后突然就宕机了会产生死锁。
过期的时间的设置上要考虑几个问题:
-
如果时间设置比较长,如果锁定发起 server 发生宕机,那么很久都解锁不了。
-
如果时间设置比较短,可能会发生业务还没有做完,发生了解锁,没有起到锁定的作用。
关于解锁:在解锁时需要判断当前的锁是不是自己所锁定的,因此需要在加锁时将 key 的 value 设置为一个随机数,在解锁时进行判断。 因为在解锁时有 get 数据再判断的多个操作,因此这里也需要防止并发问题,因此使用 lua 脚本写这个操作。如以下伪代码:
$script =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
| |
$result =
$this->redis->eval(script,array($key,$val),1);
|
注意一个可能发生的问题:
redis 的主从异步复制机制可能丢失数据,会出现如下场景:A 线程获得了锁,但锁数据还未同步到 slave 上,master 挂了,slave 顶成主,线程 B 尝试加锁,仍然能够成功,造成 A、B 两个线程并发访问同一个资源。
基于 lua 脚本实现
由于 redis 服务器的单进程单线程模型,因此产生这种被大量使用的分布式锁的方案。
lua 脚本通过 eval 或者 evalsha 方法进行执行,同时将脚本涉及到的 key 和参数传递给 redis 服务器执行。客户端可以通过 jedis 进行调用。 evalsha 是对脚本在 redis 服务器进行预编译,这样可以减少网络交互量,加速执行时的速度。
注意以下:
-
由于是分布式集群环境,如果传递了多个 key,而 key 处于不同的 slot 组服务器,那么执行将会报错。
-
lua 脚本中由于是单进程单线程执行,因此不要做消耗时间的操作。 在简单操作的情况下,在 CPU 6 核 Intel® Core™ i7-2720QM CPU @ 2.20GHz 内存 16GB 的情况下能跑出 5 万 TPS。
-
如果要进行 TPS 扩容,则需要通过 key 对应的 slot 组不同,将 lua 分发到不同的 slot 组中的 redis master 服务器去执行。
-
程序都建议使用 evalsha 的方法去执行,这样可以提高 TPS。
API 网关分布式限流
API 网关中针对一个 API、API 分组、接入应用 APP ID,IP 等进行限流。这些限流条件都将会产生一个限流使用的 key,在后续的限流中都是对这个 key 进行限流。
限流算法通常在 API 网关中可以采用令牌桶算法实现。
必须说明一点的是分布式限流由于有网络的开销,TPS 的支持隔本地限流是有差距的,因此在对于 TPS 要求很高的场景,建议采用本地限流进行处理。
下面讨论我们应该采用 redis 的哪一种分布式锁的方案:
由于 redis 事务要得到锁的效果需要在高 TPS 时会产生大量的无效的访问请求,所以不建议在这种场景下使用。
SET NX/EX 的锁方案会产生在过期时间的问题,同时也有异步复制 master 数据到 slave 的问题。相比 lua 方案会产生更多的不稳定性。
我建议采用 lua 的方案来实施分布式锁,因为都是单进程单线程的执行,因此在 TPS 上和第二种方案没有大的区别,而且由于只是一个 lua 脚本在执行,甚至是可能纯 lua 执行可能会有更高的 TPS。 当然是 lua 脚本中可能还是会去设置过期时间,但是应用 server 宕机并不会影响到 redis 中的锁。 当然 master 异步复制的问题还是有, 但是并不会造成问题,因为数据只会有 1 个 lua 脚本执行问题,下一个执行就正常了。
在实现方案的时候使用了 Jedis 库,有一些问题在方案的实现层面我已经去做过验证了,可能也会是读者的疑问。
- 理论上 jedis 都应该是连接 redis 集群中的 master 节点,因为 redis 主备是采用的主备(Active-Standby 方式)的方式。 需要确定在这种情况下应该是配置所有的 redis 节点还是只配置 master。 理论上是需要配置所有的节点。
答:配置所有节点,做自动转发。
- 需要测试在其中一个 slot 组的 master 挂掉后,slave 成为 master 后,jedis cluster 的方式还能正常的访问对应的 key。从资料上来看是能自动处理的。
答:能自动处理。
- 因为是采用 redis cluster,所以需要测试 key 存储在 slot 1 组上时,jedis cluster 连接的是 slot 2 组上的 redis 服务器,在这种情况下要能正常的调用 lua。 如果不能要看怎么样调用,因为 redis 可能会返回 "MOVED" 指令,这个要看 jedis 组件有没有实现自动的迁移。
答:能自动处理。
- 需要测试连接的是 salve 节点,但是读请求是转给了 master 节点,salve 节点不处理请求。因为是主备(Active-Standby 方式)状态。
答:能自动处理,由 Jedis jar 进行维护节点的状态,查询最新的 master 节点的信息。