一、Redis事务
Redis实现了基本的事务功能,但是不具有回滚功能。Redis通过使用 MULTI
和EXEC
两个命令来开启事务,Redis会先缓存所有包围在事务中的命令,直到EXEC
被调用才会将所有的命令一起发送给Redis服务。Redis保证事务的原子性和隔离性,也就是说事务中的命令要么全部执行,要么全部不执行,一但事务开始执行Redis服务会暂停其它客户单发送的请求,也就是事务中的命令按顺序执行并且不会被其它客户端打扰。
由于Redis事务执行过程中不加锁,所以多线程或者多客户单执行过程中会出现竞争条件。比如一个客户端A通过查询发现库存还有1个可用,于是下单,但是命令还未发往Redis服务,于此同时客户端B通过查询也发现库存还有1个可用,于是也下单,因为缓存或者网络等各种原因客户端B的命令可能会先于客户端A到达Redis服务,于是客户端B事务执行成功,客户端A产生了错误(但是它并没有意识到这点,也就是库存现在是负的)。为了解决这个问题,Redis使用了一个叫check-and-set
的WATCH
命令,也就是乐观锁。沿用上面的例子,但是这次在使用事务之前先用WATCH
命令监视库存,然后按照顺序执行,客户端B事务执行成功,客户端A事务执行失败(通过WATCH
命令可以在执行事务的时候意识到库存已经被修改),此时客户端A可以重新执行事务,然后查询得知库存为0,于是放弃事务并通知用户。WATCH
命令虽然可以保证数据正确,但是缺点是效率低,因为一但Redis负载高的话,冲突会指数上升,客户端需要不断的重复执行事务才有可能成功。
二、Redis锁
WATCH
命令虽然可以保证数据的正确性,但是效率实在是不高,所以使用锁(排它性锁)将获得更高的效率。Redis并没有打算实现这种锁,不过好在通过Redis的相关命令也是可以自己实现锁功能(通过Redis中的key来实现,成功获取锁设置一个key,释放锁删除那个key,通过判断key是否存在来确定是否可以获取锁)。自己实现锁功能需要注意的是锁的释放,比如客户端A获取了库存锁,但是突然宕机了,这样其它客户端就无法获取锁了。所以在自定义锁的时候,可以通过Redis的key过期功能,给锁(一般就是Redis中的key)添加一个过期时间,这样就算客户端宕机了锁也能自动释放。另外需要注意的是锁的过期时间,因为不是所有的操作都是简单的,有些客户端可能需要花很长的时间才能完成事务,如果客户端在事务完成前,锁就自动释放了,这样会造成数据错误。一般解决方法是在锁快要释放之前,重新刷新锁的过期时间,也就是续约锁。自己实现锁还需要考虑很多东西,好在redisson这个开源库实现了Redis锁,可以直接参考或者使用。
三、Redis分布式锁
Redis分布式锁基本上就是上面所说的自定义实现的锁,因为Redis可以被多个应用访问,所以通过Redis锁就可以让多个应用通过锁来访问资源。但是一般的Redis分布式锁都存在一个问题,就是对于故障转移的能力不够。比如Redis主服务A和从服务A1组成了一个Redis服务,客户端C申请了锁,但是在A向A1同步数据前(相对客户端来说A和A1之间同步数据是异步的)宕机了,然后根据故障转移规则A1升级成了主服务,但是这时A1是没有C所申请的锁的信息,如果这时其它客户端申请相同的锁,那么它就能获取锁,此时两个客户端都拥有了相同的锁。
针对这种情况Redis官方开发了一种叫Redlock
的分布式锁,相对于前面所说的实现(单台Redis主服务),Redlock
使用多台Redis主服务的方式来实现。简单来说如果有N台主服务,那么在指定时间范围内获取到N/2+1台主服务中的锁,那么就获取锁成功。也就是如果现在有5台主服务,那么至少要在3台主服务中获取到锁,Redlock
才算获取成功。这么做主要是为了防止某一台服务宕机后,其它服务还能正常运行。
Redlock
算法原理可以参考https://redis.io/topics/distlock
redisson这个开源库实现了Redlock
四、Redis分片
Redis实现了主从服务,但是这种方式只是扩展了读请求,对于写请求依然是单服务。为了扩展写请求需要一组相互独立Redis主服务,并且客户端需要自行将数据分片到不同的Redis主服务中。Redis分片的原理基本上就是crc32(key) mod N
,其中N表示主服务的个数。
分片在不同软件层次中实现:
1、客户端分片,由客户端实现将数据分片到哪个服务中
2、代理分片,由代理决定将数据分片到哪个服务中
3、查询路由,客户端随机访问一台服务,由服务决定是自己处理还是路由到其它服务器。Redis Cluster
实现了混合形式的查询路由,也就是在查询路由的基础上,添加一个缓存表,这张表会逐步记录key与服务器之间的关系。比如客户端首先将key1发往服务器A,然后服务器A发现key1应该由服务器B处理,它会将这个路由信息告诉客户端,客户端缓存表会记录key1由服务器B处理
,当第二次查询key1的时候,客户端会直接请求服务器B。
分片实现方案:
1、一致性哈希算法,这个算法在分布式应用中比较广泛,可以动态扩容和缩容。但是这个算法不太适合将Redis用来存储持久化数据,主要是因为这个算法没有数据迁移功能。比如原本key1存储在服务器A,扩容后key1需要存储在服务器B,这时key1的数据在逻辑上已经丢失了。这个算法最适合将Redis用来缓存数据,第一个是因为这个算法在扩容和缩容时只会影响一部分key的映射,第二个受影响的key就算丢失了数据也不会对应用造成影响,第三个通过对key设置过期时间,那些受影响的key的老数据会自动删除。
2、预分片,这个方案比较适合将Redis用来存储持久化数据。预分片的原理就是提前预测好未来需要多少台Redis服务。假设未来最多需要10台服务器,那么在一开始的时候就在一台服务器上开启10个Redis服务。等到内存不够的时候,准备一台新的服务器,将5个Redis服务迁移到新的服务器中,通过Redis的数据备份、主从复制等功能这个方案完全可以实现。一致性哈希算法也可以用作预分片的哈希算法,只要限制它扩容或缩容。
分片缺点:
1、涉及多个key的操作通常不允许,比如集合的交集、差集等操作。主要是分片后多个key不在同一服务中,分片之间也不可能移动数据。
2、同时操作多个key,则不能使用Redis事务。
3、数据备份会比较复杂,因为现在数据分散在各个分片中。
4、不太支持动态扩容或缩容,只能在一定条件下使用。
四、Redis Cluster
Redis Cluster
是官方实现的一种分片方案。它没有使用一致性哈希算法,而是使用了一种叫哈希槽的概念。Redis Cluster
一共有16384个哈希槽,然后使用CRC16(key) mod 16384
来计算每个key所对应的哈希槽。Redis Cluster
中的每个节点(redis服务)都会负责维护一部分哈希槽,比如你的集群中有3个节点,那么:
- Node A contains hash slots from 0 to 5500.
- Node B contains hash slots from 5501 to 11000.
- Node C contains hash slots from 11001 to 16383.
假如你通过CRC16(key) mod 16384
算出来的结果是5501,那么当你第一次查询的时候客户端会随机访问一个节点,比如Node A,它会首先查看5501是否由自己负责,如果是就直接处理,如果不是就告诉客户端Node B负责处理,客户端需要重新向Node B发送请求。
Redis Cluster
相比之前的分片方案,它具有高扩展性和伸缩性,一致性哈希算法也可以扩展和伸缩但是它无法迁移数据。比如现在有A、B、C三个节点,你需要添加D,那么首先将D添加到集群中,然后从A、B、C中分别迁移一部分哈希槽到D(哈希槽所关联的数据会一同转移)。如果要删除节点A,首先需要将A负责的哈希槽移动到B 、C,然后就可以关闭节点A。
Redis Cluster
允许你在一定范围内执行涉及多个key的操作,包括交集、差集、事务等操作,只要你所操作的key都关联到同一哈希槽。这个可以通过hash tags
的技术强制将不同的key映射到同一哈希槽。比如hello{word}、say{word}、test{word}
这3个key用来计算哈希槽的部分不是整体,而是包围在花括号中的word
,这样它们所关联的哈希槽就是同一个。
Redis Cluster
使用主从模式来备份数据和提供可靠性,假设有A、B、C三个主节点,以及A1、B1、C1三个从节点,那么每个节点都有一个备份从节点,如果A节点宕机了,那么A1会被推选为新的主节点替换A节点。当然如果A1也宕机了,那么集群将停止工作。
Redis Cluster
无法保证数据的强一致性
,也就是说在一些条件下集群已经对客户端确认的写入依然会丢失。假设有A、B、C三个主节点,以及A1、B1、C1三个从节点,A接收写入并且向客户端确认,在A向A1同步数据前宕机了,然后A1被推选为新的主节点替换A,那么刚刚的写入数据将丢失。类似的情况也会发生在网络分区阶段,主节点少部分的分区一般也会丢失一部分数据。不过集群对数据丢失会限制在一定时间窗口内。