一、高频面试题

问题1:Redis缓存和数据库数据不一致的常见原因有哪些?

参考答案:主要有三个常见原因。一是更新顺序问题,比如在读写并发场景下,先更新缓存后更新数据库,可能导致其他读请求拿到旧数据库数据;反过来先更新数据库再更新缓存,更新缓存前的读请求也会拿到旧数据。二是缓存失效异常,当缓存过期后,读请求从数据库加载新数据时如果写入缓存失败,就会导致缓存数据没更新。三是并发操作冲突,比如多个线程同时发现缓存失效,都去数据库读取数据并写入缓存,后写入的会覆盖先写入的,导致缓存数据比数据库旧。

第一步追问:能举个具体场景说明更新顺序问题吗?
参考答案:比如电商场景中,用户下单后需要扣减库存。如果先更新Redis缓存的库存数量,再更新数据库,此时若数据库更新失败,缓存却已经是扣减后的数值,后续读请求就会读到错误的库存数据,导致数据不一致。

第二步追问:缓存失效异常和更新顺序问题的本质区别是什么?
参考答案:缓存失效异常的核心是时间差导致的操作中断,比如缓存过期后,应用从数据库读取数据时网络延迟或写入缓存时程序报错,导致缓存未更新;而更新顺序问题的核心是操作顺序本身的逻辑缺陷,即使每个操作都成功,只要顺序不对,在并发场景下就会引发不一致。

第三步追问:如何从架构设计层面避免并发操作冲突?
参考答案:可以引入分布式锁,确保同一时间只有一个线程能操作缓存和数据库,比如用Redis的SET NX命令实现锁机制;或者采用队列化处理,将并发的读写请求按顺序排队执行,减少并行冲突。

问题2:线上排查Redis和数据库数据不一致时,通常有哪些步骤?

参考答案:第一步是数据对比校验,通过脚本定期全量或抽样对比两者的关键数据,比如用Python写脚本遍历数据库表和Redis键值,记录不一致的字段。第二步是日志分析,查看应用层、Redis和数据库的日志,定位发生不一致的时间点,比如应用是否在某个时刻频繁报错,Redis是否有连接超时记录。第三步是复现与调试,根据日志信息在测试环境模拟相同场景,观察数据更新流程是否符合预期,比如模拟高并发下的读写操作,看缓存和数据库是否同步。

第一步追问:数据对比时,全量对比和抽样对比分别适用于什么场景?
参考答案:全量对比适合数据量小或首次排查的场景,能全面发现问题,但耗时久;抽样对比适合数据量大的场景,比如按一定比例(如10%)抽取数据对比,效率更高,常用于日常巡检。如果抽样发现不一致,再扩大范围排查。

第二步追问:日志分析时,重点关注哪些类型的日志?
参考答案:重点看三类日志:一是应用日志,是否有更新缓存或数据库时的异常堆栈(如NullPointerException);二是Redis日志,是否有主从同步延迟、内存不足导致的淘汰日志;三是数据库慢查询日志,是否有更新操作执行时间过长,导致其他请求读到旧数据。

第三步追问:如果复现问题后发现是缓存更新逻辑错误,该如何优化?
参考答案:可以改用删除缓存而非更新缓存的策略,比如写操作时先更新数据库,再删除对应的Redis键,这样读请求下次会主动从数据库加载最新数据。同时,添加重试机制,如果删除缓存失败,通过消息队列异步重试,确保最终一致性。

二、真实技术案例

某电商平台在一次大促活动期间,用户反馈商品详情页展示的商品价格与下单时的价格不一致。例如,商品详情页显示价格为99元,但下单结算时价格却变成了199元。这种情况不仅严重影响用户体验,还可能引发用户投诉和信任危机。运营人员紧急反馈后,技术团队立即介入调查。

2、故障分析

2.1 业务流程梳理

该电商平台商品信息展示业务流程如下:用户访问商品详情页时,系统先从Redis缓存中查询商品信息,如果缓存中存在则直接返回;若缓存中不存在,则从MySQL数据库查询商品信息,查询到后将数据存入Redis缓存,再返回给用户。当商品价格发生变更时,业务系统会先更新MySQL数据库中的价格,再尝试更新Redis缓存。

2.2 可能原因推测

基于上述业务流程,初步推测导致数据不一致的原因可能有以下几种:

  1. 缓存更新失败:在更新商品价格时,更新MySQL数据库成功,但更新Redis缓存失败,后续用户查询时获取到的是旧的缓存数据。
  2. 并发问题:在高并发场景下,多个线程同时操作缓存和数据库,可能出现缓存和数据库更新顺序错乱,导致数据不一致。例如,一个线程正在更新数据库,另一个线程此时查询缓存,由于缓存未更新,获取到的是旧数据;而当数据库更新完成后,缓存却没有及时同步。
  3. 缓存过期:如果缓存设置了过期时间,在缓存过期瞬间,多个用户同时请求,可能导致大量请求穿透到数据库,并且在数据库查询结果写入缓存时出现顺序问题,造成部分用户获取到不一致的数据。

3、故障定位

3.1 查看日志

首先,开发人员查看业务系统日志,发现存在大量更新Redis缓存失败的异常日志。例如,在商品价格更新操作的日志中,记录了类似如下信息:

// 模拟更新Redis缓存的代码
public void updateProductPriceInRedis(Product product) {try {// 假设redisTemplate是Redis操作模板redisTemplate.opsForValue().set("product:" + product.getId(), JSON.toJSONString(product));} catch (RedisConnectionFailureException e) {// 捕获Redis连接失败异常并记录日志log.error("更新商品{}到Redis缓存失败", product.getId(), e);}
}

从日志中可以明确,缓存更新失败是导致数据不一致的一个重要原因。

3.2 分析并发情况

为了进一步分析是否存在并发问题,开发人员使用了Java的并发分析工具JProfiler对系统进行监控。在高并发测试环境下,发现当多个线程同时执行商品信息查询和更新操作时,确实出现了缓存和数据库更新顺序错乱的情况。例如,在某个时间点,线程A正在更新MySQL数据库中的商品价格,此时线程B查询Redis缓存,由于缓存未更新,获取到旧价格;随后线程A更新完数据库,但在更新Redis缓存之前,线程B又发起了一次查询,依然获取到旧的缓存数据,从而导致数据不一致。

3.3 检查缓存过期设置

查看商品信息缓存的过期时间设置,发现设置为30分钟。在大促期间,商品价格变动频繁,30分钟的过期时间可能导致在缓存过期时,大量用户请求同时查询数据库并更新缓存,引发数据不一致问题。通过分析缓存过期时间内的请求日志,确认了在缓存过期瞬间,确实出现了部分用户获取到不一致数据的情况。

4、解决方法

4.1 缓存更新失败解决方案

针对缓存更新失败的问题,采用重试机制。当更新Redis缓存失败时,进行多次重试,并记录重试次数和结果。代码实现如下:

public void updateProductPriceInRedisWithRetry(Product product, int maxRetry) {int retryCount = 0;boolean success = false;while (retryCount < maxRetry &&!success) {try {redisTemplate.opsForValue().set("product:" + product.getId(), JSON.toJSONString(product));success = true;} catch (RedisConnectionFailureException e) {retryCount++;if (retryCount == maxRetry) {log.error("重试{}次后,更新商品{}到Redis缓存仍失败", maxRetry, product.getId(), e);} else {log.warn("更新商品{}到Redis缓存失败,进行第{}次重试", product.getId(), retryCount);}}}
}

同时,为了避免因Redis服务长时间不可用导致大量重试请求积压,设置一个重试间隔时间,例如每次重试间隔500毫秒:

public void updateProductPriceInRedisWithRetry(Product product, int maxRetry) {int retryCount = 0;boolean success = false;while (retryCount < maxRetry &&!success) {try {redisTemplate.opsForValue().set("product:" + product.getId(), JSON.toJSONString(product));success = true;} catch (RedisConnectionFailureException e) {retryCount++;if (retryCount == maxRetry) {log.error("重试{}次后,更新商品{}到Redis缓存仍失败", maxRetry, product.getId(), e);} else {log.warn("更新商品{}到Redis缓存失败,进行第{}次重试", product.getId(), retryCount);try {// 每次重试间隔500毫秒Thread.sleep(500);} catch (InterruptedException interruptedException) {Thread.currentThread().interrupt();}}}}
}
4.2 并发问题解决方案

为了解决并发问题,引入分布式锁。使用Redisson框架实现分布式锁,确保在同一时刻只有一个线程能够更新缓存和数据库。首先,在项目的pom.xml文件中添加Redisson依赖:

<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.16.1</version>
</dependency>

然后,配置Redisson客户端,在application.yml文件中添加如下配置:

redisson:singleServerConfig:address: "redis://127.0.0.1:6379"password: ""database: 0

使用Redisson实现分布式锁的代码如下:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service
public class ProductService {@Resourceprivate RedissonClient redissonClient;@Resourceprivate ProductRepository productRepository;@Resourceprivate RedisTemplate redisTemplate;public void updateProduct(Product product) {// 锁的名称,可根据业务需求自定义RLock lock = redissonClient.getLock("product:" + product.getId() + ":lock");try {// 尝试获取锁,等待时间为10秒,锁过期时间为30秒boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);if (isLocked) {// 更新数据库productRepository.save(product);// 更新缓存updateProductPriceInRedisWithRetry(product, 3);}} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {if (lock.isHeldByCurrentThread()) {// 释放锁lock.unlock();}}}
}
4.3 缓存过期问题解决方案

为了避免缓存过期瞬间大量请求穿透到数据库,可以采用缓存预热和随机过期时间的策略。缓存预热即在系统启动时,提前将热点商品信息加载到Redis缓存中。随机过期时间是指在设置缓存过期时间时,在一个范围内随机生成一个时间,避免大量缓存同时过期。

缓存预热的代码实现如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;import java.util.List;@Component
public class CachePreheating implements CommandLineRunner {@Autowiredprivate ProductRepository productRepository;@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic void run(String... args) throws Exception {List<Product> hotProducts = productRepository.findHotProducts(); // 假设该方法获取热点商品列表for (Product product : hotProducts) {redisTemplate.opsForValue().set("product:" + product.getId(), JSON.toJSONString(product));}}
}

设置随机过期时间的代码如下:

import java.util.Random;public void setProductCacheWithRandomExpire(Product product) {Random random = new Random();// 随机过期时间范围为15 - 45分钟int expireTime = random.nextInt(30) + 15;redisTemplate.opsForValue().set("product:" + product.getId(), JSON.toJSONString(product), expireTime, TimeUnit.MINUTES);
}

5、预防策略

5.1 定期数据校验

建立定期的数据校验机制,定时对比Redis缓存和MySQL数据库中的数据。可以使用定时任务,例如Spring Boot中的@Scheduled注解,每隔一段时间(如每小时)对商品信息进行一次全量或抽样校验。代码实现如下:

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.List;@Component
public class DataConsistencyCheck {@Resourceprivate ProductRepository productRepository;@Resourceprivate RedisTemplate redisTemplate;// 每小时执行一次数据校验@Scheduled(cron = "0 0 * * * *")public void checkProductDataConsistency() {List<Product> products = productRepository.findAll();for (Product product : products) {String cacheValue = (String) redisTemplate.opsForValue().get("product:" + product.getId());if (cacheValue != null) {Product cacheProduct = JSON.parseObject(cacheValue, Product.class);if (!cacheProduct.getPrice().equals(product.getPrice())) {// 数据不一致,进行处理,例如重新更新缓存updateProductPriceInRedisWithRetry(product, 3);}}}}
}
5.2 监控与告警

搭建完善的监控与告警体系,对Redis缓存和数据库的操作进行实时监控。监控指标包括Redis连接状态、缓存命中率、数据库读写性能等。当出现异常情况,如Redis连接失败次数超过阈值、缓存命中率过低等,及时通过邮件、短信或企业微信等方式发送告警信息,以便运维和开发人员及时处理。可以使用Prometheus和Grafana搭建监控平台,通过编写自定义的监控脚本采集Redis和数据库相关指标数据,并在Grafana中进行可视化展示和告警配置。

5.3 代码审查与规范

加强代码审查,制定严格的缓存和数据库操作代码规范。要求开发人员在操作缓存和数据库时,遵循统一的流程和模式,例如先更新数据库,再更新缓存,并添加必要的异常处理和重试机制。同时,定期组织代码 review,及时发现和解决潜在的数据一致性问题。