Redis缓存数据一致性解决方案分析

卡二条的技术圈

共 3048字,需浏览 7分钟

 · 2021-02-08

文章简介

Redis作为一个非关系型数据库,已经被应用在各种高性能的业务场景。Redis是一个基于内存性质的数据库,因此在读写上面都是有着非常不错的性能,在实际的使用过程中,大多数也是用在一些业务数据缓存的情况。

设计到缓存的情况,我们就不得不考虑一个情况,就是缓存数据的一致性。如何理解缓存的一致性呢?举一个简单的例子,在一个电商系统应用中,我们将商品的库存数量存在缓存中,此时我们在后台更新了商品的库存数量,如何保证缓存中的库存信息同步更新并且不会出现库存数量问题?文章后面在代码演示,也以该案例作为演示。

缓存设计

了解缓存设计之前,我们先看看下面的一张图。这张图也是很多缓存系统的一个设计模式。

  1. 客户端向服务端发送请求。直接去缓存中查询数据。

  2. 如果缓存中存在数据,则直接返回给客户端缓存中的数据。

  3. 如果缓存中不存在数据,则查询数据库。

  4. 根据MySQL中查询的数据,写入缓存并返回给客户端。

文章主旨

文章前面提到的数据一致性,指的是MySQL与缓存中数据如何保持同步。后面文章也是针对如何去实现数据同步进行分析。

更新策略

先缓存后数据库

策略说明
  1. 后端发生更新请求,更新对应的Redis缓存。在这个过程中可以直接删除,再新写入;也可以采用更新的方式。使用删除相对更为便捷。

  2. 如果缓存更新失败,直接返回客户端错误信息。

  3. 如果缓存更新成功,则执行更新MySQL操作。

  4. 如果MySQL更新失败,则回滚整个更新,包括缓存中的更新操作。

问题分析
  1. 如果在第1中采用的删除缓存,当第2中更新缓存失败,此时需要手动的去追加缓存,否则会出现缓存击穿情况,这种情况是非常严重的。

  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 '缓存更新失败';

先数据库后缓存

策略说明
  1. 客户端发起更新请求,先更新MySQL。

  2. MySQL更新成功之后,接着更新缓存。更新缓存可以直接使用删除操作,也可以指定更新。

  3. 如果Redis更新失败则返回客户端信息。

问题分析
  1. 该策略能够很明显的看出,在更新MySQL阶段是没问题的。MySQL失败直接返回客户端更新失败,也不需要去操作缓存。

  2. 但是当更新缓存时,如果缓存更新失败,但是MySQL中的数据是更新成功了。这样就面临这一个问题,到底是回滚还是不做任何操作呢?

  3. 如果第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 '数据更新失败';

多线程同步

策略说明
  1. 客户端发起请求,此时创建两个线程。

  2. 一个线程执行MySQL更新,一个线程执行缓存更新。

  3. 如果两个线程有一个不成功,则回滚整个更新操作。

问题分析
  1. 该策略通过多个线程更新数据,减少阻塞问题,加快程序处理速度。

  2. 如果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 '数据更新失败';

加锁处理

策略说明
  1. 客户端发起请求,创建一个锁。

  2. 此时依次更新MySQL和缓存数据。

  3. 不管成功和失败,执行完之后就释放锁。

问题分析
  1. 客户端发起请求,创建一个锁。在创建锁的时候,可以使用set-nx方式,避免服务挂掉缓存不会自动过期。

  2. 更新MySQL和缓存数据。

  3. 缓存成功则释放锁,缓存失败则释放锁。

  4. 该方式适合数据高度一致性的情况,例如后端在发起请求时,客户端就不能进行读操作,直到写操作成功或者失败后释放锁。

  5. 使用该方式,需要客户端读代码判断锁情况处理。存在锁则处于等待情况。不适合高并发的业务场景。但是保证了数据的完全一致。

代码演示
// 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在更新缓存。

推荐阅读

Redis事务处理机制分析与总结

Redis数据类型应用场景总结

Redis的过期策略和内存淘汰策略最全总结与分析



浏览 40
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报