在很多系统中重要数据通常都是写入关系数据库如mysql中,为了实现读写分离,提高系统负载能力,缩短响应时间通常还需要用到缓存。
缓存带来了系统性能的提升同时也把数据一致性问题摆在了开发者面前,在数据库使用读写分离和主从同步的情况下这种一致性问题会变得更加复杂。本文将介绍几种提升一致性的方案供大家参考。
背景介绍
一般使用缓存(本文中的缓存不特指某一种分布式缓存或本地缓存)的方式为在读数据时首先读取缓存,如果缓存没有则读数据库然后将数据写入缓存最后返回;写数据时首先清除缓存内的数据,然后写数据库。
这种方式在数据库配置了主从库时会遇到数据不一致的问题,首先来看一下这种实现的具体流程如下图:
图1 主从数据不一致
在读数据时如果缓存中没有数据则读取从库的数据,然后写入缓存中并返回;在写数据时先清除缓存中的数据,然后将数据写入主库,主库数据会被同步到从库。
这种实现方式的主要问题在于当数据写入主库后,缓存没有数据,这时读请求会读取从库的数据。此时如果发生主从延迟,主库的数据还没有写到从库,则应用服务器会将从库读到的脏数据写入缓存服务器中,如果写入的数据没有加有效期或有效期很长就会造成数据不一致,如果主从延迟时间较长可能会导致大面积的数据不一致。下面将介绍几种解决数据一致性问题的方案。
加有效期
给缓存中的数据增加有效期是解决一致性问题最简单的确保数据最终一致性的方法,这种方法在缓存中没有数据需要查询数据库时将查询结果放入缓存的时候设置一个有效期(更新数据时仍然先清除缓存数据),非常适用于更新频率较低的数据,例如商品信息。
但是单纯给数据加上有效期也存在一些明显的问题,如果有效期较长就会出现上面提到的数据不一致的问题,如果有效期较短就会出现缓存效率不高经常读库的情况。在使用这种方法的时候就需要我们根据数据的更新频率确定合适的有效期时间,当冷热数据并存时这种方案就显得难以兼顾。那么什么策略既能确保数据的最终一致性又能充分利用缓存呢?这就要提到业内使用最多的双淘汰了。
双淘汰
双淘汰与本文第一个方案相比在读取数据时是相同的,区别在于更新数据的流程。在更新数据时仍然首先清除缓存的数据,然后将数据写入到数据库中,然后将数据记录在一个延迟队列或哈希表中,同时另一个线程不断读取延迟队列或者哈希表,根据数据存入的时间也预先设定的延迟时间再次清除缓存了的数据。
可以看出预先设定的延迟时间应该大于数据库主从同步较慢情况下的同步时间,这样就能确保在主从延迟的情况下缓存中的脏数据也能被清除保证了数据一致性。流程如下图,C语言中可以使用哈希表实现。
虽然双淘汰保证了数据的最终一致性并提高了缓存的使用率,但在两次“淘汰”之间读取的数据仍然有可能是脏数据,这种情况会在主从延迟较长的情况下尤为明显。对于某些对实时一致性要求较高的系统如何获得更好的读一致性呢,这里需要提到双淘汰的另一种变型。
图2 双淘汰
另一种双淘汰
为了提高读到数据的准确性这种方法在更新数据时首先清除缓存数据并在缓存中存入这个数据对应的标记,在写入数据库成功后再将数据写入到一个延迟队列或哈希表中。在读取数据时从缓存读取数据,如果存在直接返回,如果不存在则读取数据对应的标记,如果标记存在则读主库否则读从库,最后将数据写入缓存中。
同时另一个线程不断读取延迟队列或者哈希表,根据数据存入的时间也预先设定的延迟时间再次清除缓存了的数据。整个读写过程如下图。可以看出这种方法通过在缓存增加一个标记将部分读请求分流到了主库,这个标记可以是数据的主键或其他唯一标识,通过牺牲一部分主库的性能提高了读请求的数据一致性。
图3 双淘汰2
目前为止没有哪一种缓存策略是万能的,基本上我们仍需要根据具体的业务场景和数据类型选择合适的缓存策略。数据量越大数据情况也复杂通常就需要越复杂的缓存策略,希望本文介绍的几个方案对读者今后的开发有所帮助。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/7086.html