Redis缓存数据一致性分析

文章简介

Redis作为一个非关系型数据库,已经被应用在各种高性能的业务场景。Redis是一个基于内存性质的数据库,因此在读写上面都是有这非常不错的性能,在实际的使用过程中,大多数也是用在一些业务数据缓存的情况。一般团队都是自己搭建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在更新缓存。

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
Redis缓存数据一致性分析
Redis作为一个非关系型数据库,已经被应用在各种高性能的业务场景。Redis是一个基于内存性质的数据库,因此在读写上面都是有这非常不错的性能,在实际的使用过程...
<<上一篇
下一篇>>