Redis 在 3.0 前通常有两种集群方案,一是 proxy(Twemproxy、Codis),二是使用 Sentinel(哨兵)。 经过 Sentinel 是一种使用哨兵来达到高可用的方案,而 proxy 是用于在前置上进行 sharding 用代理给后端的 redis 集群的方案,达到负载均衡的方案,在单个分片的 redis 中做主从。 由于本文要重点讲解的不是 3.0 前的方案,所以说的比较粗略。redis
Redis3.0 提供了官方的 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 状态的条件:网络
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 的分布式锁方案。
严格来讲这并非分布式锁,只是经过改造能够实现锁的效果。这里并非实现锁定其余的线程被阻塞的效果,而是若是数据被其余客户端修改了就返回失败。原理是基于 reids 的 multi 和 watch 命令。 在事务开始前对要锁到的数据进行 watch,进行业务操做后,若是发现锁定的数据已经变了,就提交失败,从新进行业务操做。 在这个方案中若是执行失败就一直反复执行直到成功,也是实现了多个 redis 客户同时修改一个数据时的协调的锁的功能。
伪代码以下:
复制代码
jedis.set("balance",String.valueOf(balance)); jedis.watch("balance"); Transaction transaction = jedis.multi(); transaction.decrBy("balance",amtToSubtract);/ transaction.incrBy("debt",amtToSubtract); List result = transaction.exec();// 执行事务 if(result==null){ // 从新执行事务或者其余。 }else{ // 事务执行成功。 }
Redis set key 时的一个 NX 参数能够保证在这个 key 不存在的状况下写入成功。而且再加上 EX 参数可让该 key 在超时以后自动删除。
jedis 伪代码:
复制代码
Stringset(Stringkey,Stringvalue,Stringnxxx,Stringexpx,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 两个线程并发访问同一个资源。
因为 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 分组、接入应用 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 jar 进行维护节点的状态,查询最新的 master 节点的信息。