如今,Redis已成为最浏览的缓存解决方案之一,尽管关系型数据库带了许多很棒的功能,如ACID。但是,为了使用这些功能,数据库的性能在高负载的情况下也会有所下降。
为了解决这个问题,许多公司和网站在应用层和数据访问层之间都会增加一个缓存层。通常使用内存中缓存来实现这个缓存层。正如我们所知,传统的关系型数据库的性能瓶颈通常是存储I/O。由于科技的发展和进步,主存储器的价格一直在下降,增加内存已经不是什么难事了,因此现在可以在内存中缓存一部分热点数据来提供性能。
背景
虽然我们可以把热点数据存储在内存中,但是这种方法已经会让人头疼,因为我们失去了对数据单一源的控制,相同的数据存储在数据库和内存中。如何避免阻塞情况下确保Redis中的数据和数据库中的数据一致?
下面,我们将了解一些常用的解决方案,这些方案大部分情况下几乎是正确的,因为它们可以保证99.9%的情况下Reids和数据库中的数据一致。但是高并发情况下就可能出现脏数据。
缓存过期
通常我们常用的方案可能就是缓存过期,但是不得不承认这是保证一致性的糟糕方案。
例如:我们设置缓存过期时间为30分钟,你就要确保这30分钟内不会读取到脏数据。如果将过期时间设置得更短会不会好点?
如果你的网站具有巨大的流量和高并发服务,这样你确实缩短了不一致的可能,但是已经违背了使用缓存的初衷,可能会有很多缓存违背命中。
暂存
暂存模式通常是这样的:
对于不可变的操作(读取):
缓存命中:直接从Redis中返回数据,无需查询MySql缓存未命中:查询MySql(可以使用只读来提高性能),将返回的数据放到Redis中,然后返回结果给客户端对于可变操作(创建、更新、删除):
创建、更新、删除MySql中的数据删除Redis中的数据,总是删除而不是更新缓存,下一个缓存未命中将插入新值这种方法通常被我们使用,实际上,它是MySql和Redis之间实现缓存一致性的标准。但是,这种方法也存在一些问题:
正常情况下,假设写入MySql/Redis绝对不会失败,它通常可以保证最终一致性。假设我们有个热门服务,做了负载分别放在A、B两个服务器上,在某个时刻A已经成功更新了MySql中的数据。在删除Reids中的数据之前,B尝试读取这个数据,然后B将命中缓存,因为辞职A还没有来得及删除Redis中的数据。因此B就读取到了脏数据,但是Redis中的数据最终还是会被删除,其他服务最终将读取到更新后的数据。在极端情况下,它也不能保证最终的一致性。同样的情况,如果A在尝试删除Redis中的数据前刚好被kill掉,这样Redis中的数据将无法被删除。这样其他服务器都会读取到脏数据。即使在正常情况下,也存在极低的可能性,最终一致性得不到保障。假设A尝试读取数据,缓存未命中,然后从MySql中读取数据。此时,由于高并发和巨大的流量导致A的服务器突然卡了。这是B尝试更新相同的数据,D更新MySql,并删除了Redis中的数据。之后A恢复并将其查询结果保存到了Redis。这样后面其他服务都会读出到脏数据,虽然这种可能性非常低。暂存-变体1
暂存模式-变体1为:
对于不可变操作(读取):
缓存命中:直接从Redis中返回数据,无需查询MySql缓存未命中:查询MySql(可以使用只读来提高性能),将返回的数据放到Redis中,然后返回结果给客户端对于可变的操作(创建、更新、删除):
删除Redis中的数据创建、更新、删除MySql数据这种方案也是非常糟糕的。假设A尝试更新数据,在某个时刻,A已经成功删除了Reids中的数据。在A更新MySql中的数据之前,B尝试读取相同的数据,且缓存未命中。然后B查询MySql并将数据保存到Redis中。注意,此时MySql中的数据尚未更新。由于A后面不会再删除Redis中的数据,因此旧的数据依旧保存在Redis中了。
暂存-变体2
暂存模式-变体1为:
对于不可变操作(读取):
缓存命中:直接从Redis中返回数据,无需查询MySql缓存未命中:查询MySql(可以使用只读来提高性能),将返回的数据放到Redis中,然后返回结果给客户端对于可变的操作(创建、更新、删除):
创建、更新、删除MySql数据在Redis中创建、更新、删除数据这也是一个不好的解决方案。假设,A、B都试图更新数据,A在B之前更新了MySql。但是B会在A之前更新Redis。最终,Mysq中的数据会由B更新。但是Redis中的数据则由A更新,这将导致不一致。
通读
通读模式为:
对于不可变的操作(读取):
客户端始终从Redis中读取。缓存未命中,这Redis应具有自动从数据库中读取的功能。对于可变的操作(创建、更新、删除):
此策略不处理可变操作。它与只写模式结合使用直写
只写模式为:
对于不可变的操作(读取):
此策略不处理不变的操作。它与通读模式结合使用对于可变的操作(创建、更新、删除):
客户端仅在Redis中创建、更新、删除数据。Redis必须原子地把数据同步到MySql直接模式的缺点非常明显,首先大部分的缓存中间件并不支持此功能。其次,Redis是缓存而不是RDBMS。Redis的主要目的并不是弹性扩展,因此在更改复制到MySql之前,很可能会丢失。即使Reids现在支持RDB和AOF之类的持久化技术,但是仍然不建议使用。
后写
对于不可变的操作(读取):
此策略不处理不变的操作。它与通读模式结合使用对于可变的操作(创建、更新、删除):
客户端需要在Redis中创建、更新、删除数据。Redis将更改放到消息队列中,然后返回成功给客户端。更改被异步复制到MySql中。后写模式与直接模式不同的时,它异步地将更改复制到MySql,这样客户端就不必等待。所以提供了吞吐量。Redis从5.0开始支持Redis流,这可能是一个不错的方式,为了提供性能,可以合并更改并批量更新到MySql中。后写模式的缺点同样也是许多缓存中间件并不支持此功能。其次,使用消息队列必须是FIFO,保证最终结果。而且还要保证消息队列的并发等,如MQ。
双删
双删模式为:
对于不可变的操作(读取):
缓存命中:直接从Redis中返回数据,无需查询MySql缓存未命中:查询MySql(可以使用只读来提高性能),将返回的数据放到Redis中,然后返回结果给客户端对于可变的操作(创建、更新、删除):
删除Redis中的数据创建、更新、删除MySql中的数据sleep一段时间如ms再次删除Redis中的数据这种模式目前是最被接受的方案。它结合了暂存-变体1,由于它是基于暂存-变体1改进的,因为在大多数情况下它可以保证最终一致性。它也通过sleep来确保删除了脏数据。尽管仍然存在极端的情况会破坏最终一致性,但是这个可能性很小。
后写-变体1
来自阿里巴巴canal项目的一种新颖模式,它是通过另外一种方式执行复制。它没有直接把Redis中的更改复制到MySql,而是通过MySql的binlog将数据复制到Redis。与后写模式相比,这更好地保证了持久性和一致性。由于binlog是RDMS技术的一部分,因此它非常具有弹性,而且这种技术,早就被用在MySql之间做主从同步。
结论
对于实际情况而言,99.9%的正确性已经足够了,我们应该谨记使用Redis的最初目的。