文章简介
Redis作为一个非关系型数据库,已经被应用在各种高性能的业务场景。Redis是一个基于内存性质的数据库,因此在读写上面都是有着非常不错的性能,在实际的使用过程中,大多数也是用在一些业务数据缓存的情况。
设计到缓存的情况,我们就不得不考虑一个情况,就是缓存数据的一致性。如何理解缓存的一致性呢?举一个简单的例子,在一个电商系统应用中,我们将商品的库存数量存在缓存中,此时我们在后台更新了商品的库存数量,如何保证缓存中的库存信息同步更新并且不会出现库存数量问题?文章后面在代码演示,也以该案例作为演示。
缓存设计
了解缓存设计之前,我们先看看下面的一张图。这张图也是很多缓存系统的一个设计模式。
客户端向服务端发送请求。直接去缓存中查询数据。
如果缓存中存在数据,则直接返回给客户端缓存中的数据。
如果缓存中不存在数据,则查询数据库。
根据MySQL中查询的数据,写入缓存并返回给客户端。
文章主旨
文章前面提到的数据一致性,指的是MySQL与缓存中数据如何保持同步。后面文章也是针对如何去实现数据同步进行分析。
更新策略
先缓存后数据库
策略说明
后端发生更新请求,更新对应的Redis缓存。在这个过程中可以直接删除,再新写入;也可以采用更新的方式。使用删除相对更为便捷。
如果缓存更新失败,直接返回客户端错误信息。
如果缓存更新成功,则执行更新MySQL操作。
如果MySQL更新失败,则回滚整个更新,包括缓存中的更新操作。
问题分析
如果在第1中采用的删除缓存,当第2中更新缓存失败,此时需要手动的去追加缓存,否则会出现缓存击穿情况,这种情况是非常严重的。
在第4中,更新MySQL失败的情况下,会回滚缓存中的数据。如果在更新MySQL操作过程中,客户端发生了新的请求,此时客户端读取到的是新数据,然而实际MySQL更新是失败的,不可能让用户读取到新数据,这样数据也会发生不一致。
代码演示
-
// Redis连接对象
-
$redis = null;
-
// MySQL连接对象
-
$mysql = null;
-
// 客户端请求参数
-
$requestParams = [];
-
// 删除缓存
-
$updateRedis = $redis->del(
'key');
-
if ($updateRedis) {
-
// 更新MySQL
-
$updateMysql = $mysql->update(
'update xxx set a=xx where id=xxx');
-
if ($updateMysql) {
-
return
'数据更新失败';
-
}
-
// 回滚缓存(由于缓存删除失败,此时就不需要手动回滚。如果是执行的更新Redis,还需要手动回滚Redis)
-
$redis->set(
'key', $requestParams);
-
}
-
return
'缓存更新失败';
先数据库后缓存
策略说明
客户端发起更新请求,先更新MySQL。
MySQL更新成功之后,接着更新缓存。更新缓存可以直接使用删除操作,也可以指定更新。
如果Redis更新失败则返回客户端信息。
问题分析
该策略能够很明显的看出,在更新MySQL阶段是没问题的。MySQL失败直接返回客户端更新失败,也不需要去操作缓存。
但是当更新缓存时,如果缓存更新失败,但是MySQL中的数据是更新成功了。这样就面临这一个问题,到底是回滚还是不做任何操作呢?
如果第2中,操作缓存失败,不做任何处理则缓存永远是旧数据,除非缓存的有效期到了。
代码演示
-
// Redis连接对象
-
$redis = null;
-
// MySQL连接对象
-
$mysql = null;
-
// 客户端请求参数
-
$requestParams = [];
-
// 更新MySQL
-
$updateMysql = $mysql->update(
'update xxx set a=xx where id=xxx');
-
if ($updateMysql) {
-
// 更新缓存
-
$updateRedis = $redis->set($requestParams);
-
if ($updateRedis) {
-
return
'数据更新成功';
-
}
-
return
'缓存更新失败';
-
}
-
return
'数据更新失败';
多线程同步
策略说明
客户端发起请求,此时创建两个线程。
一个线程执行MySQL更新,一个线程执行缓存更新。
如果两个线程有一个不成功,则回滚整个更新操作。
问题分析
该策略通过多个线程更新数据,减少阻塞问题,加快程序处理速度。
如果MySQL线程更新速度失败并且处理的速度很慢,Redis更新成功处理速度快。此时做回滚,在更细过程中,新请求从缓存中得到的是新数据,回滚之后缓存的数据又是旧数据。
代码演示
-
// Redis连接对象
-
$redis = null;
-
// MySQL连接对象
-
$mysql = null;
-
// 客户端请求参数
-
$requestParams = [];
-
// 线程一更新MySQL
-
$updateMysql = $mysql->update(
'update xxx set a=xx where id=xxx');
-
// 线程二更新缓存
-
$updateRedis = $redis->set(
'key', $requestParams);
-
if ($updateMysql && $updateRedis) {
-
return
'数据更新成功';
-
}
-
// 执行数据回滚
-
.....
-
return
'数据更新失败';
加锁处理
策略说明
客户端发起请求,创建一个锁。
此时依次更新MySQL和缓存数据。
不管成功和失败,执行完之后就释放锁。
问题分析
客户端发起请求,创建一个锁。在创建锁的时候,可以使用set-nx方式,避免服务挂掉缓存不会自动过期。
更新MySQL和缓存数据。
缓存成功则释放锁,缓存失败则释放锁。
该方式适合数据高度一致性的情况,例如后端在发起请求时,客户端就不能进行读操作,直到写操作成功或者失败后释放锁。
使用该方式,需要客户端读代码判断锁情况处理。存在锁则处于等待情况。不适合高并发的业务场景。但是保证了数据的完全一致。
代码演示
-
// Redis连接对象
-
$redis = null;
-
// MySQL连接对象
-
$mysql = null;
-
// 客户端请求参数
-
$requestParams = [];
-
/ 客户端发起请求加锁
-
// 更新MySQL
-
$updateMysql = $mysql->update(
'update xxx set a=xx where id=xxx');
-
$updateRedis = $redis->set(
'key', $requestParams);
-
if ($updateMysql && $updateRedis) {
-
// 释放锁
-
// 返回信息
-
return
'数据更新成功';
-
}
-
// 释放锁
-
// 返回信息
-
return
'更新失败';
文章总结
该文属于针对不同情况的分析。很多情况也只是出于一种理论的状态。比较推荐的方式,还是推荐使用先更新MySQL在更新缓存。
推荐阅读
转载:https://blog.csdn.net/qq_27681741/article/details/113623066