面试题答案
一键面试连锁更新可能出现的场景
- 多层缓存依赖:当应用中有多层缓存结构,例如一个主缓存依赖于另一个辅助缓存。假设主缓存存储用户基本信息,辅助缓存存储用户扩展信息。如果主缓存过期,应用在获取用户信息时发现主缓存缺失,会去查询数据库并更新主缓存。同时,在更新主缓存后,可能还需要根据主缓存中的某些标识去更新辅助缓存。若辅助缓存更新又依赖其他缓存更新,就可能形成连锁反应。
- 复杂业务逻辑关联:在电商场景中,商品详情缓存可能依赖于库存缓存、价格缓存等。当库存发生变化,库存缓存更新后,可能触发商品详情缓存更新。而商品详情缓存更新后,可能又影响促销活动缓存(因为促销活动可能依赖商品库存和价格等信息),进而形成连锁更新。
避免Redis连锁更新的方法
- 缓存过期时间随机化:
- 原理:为每个缓存设置一个随机的过期时间,而不是固定的过期时间。这样可以避免大量缓存同时过期,从而减少连锁更新的可能性。
- 示例:原本商品缓存过期时间都设置为3600秒,可以改为在3000 - 4200秒之间随机取值。在代码中(以Python为例):
import redis
import random
r = redis.Redis(host='localhost', port=6379, db = 0)
expire_time = random.randint(3000, 4200)
r.setex('product:1', expire_time, 'product_info')
- 使用队列解耦:
- 原理:将缓存更新操作放入消息队列(如Kafka、RabbitMQ等)。当缓存需要更新时,不是直接进行连锁更新,而是将更新任务发送到队列中。然后由专门的消费者从队列中取出任务并依次处理,这样可以将原本可能的连锁更新转化为串行处理,避免同时更新多个缓存引发的连锁反应。
- 示例:以Python和RabbitMQ为例,使用
pika
库:
import pika
# 连接RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# 声明队列
channel.queue_declare(queue='cache_update')
# 发送更新任务到队列
message = 'update_product_cache:1'
channel.basic_publish(exchange='', routing_key='cache_update', body=message)
print(" [x] Sent %r" % message)
connection.close()
消费者代码:
import pika
import redis
r = redis.Redis(host='localhost', port=6379, db = 0)
def callback(ch, method, properties, body):
print(" [x] Received %r" % body)
# 解析消息并执行缓存更新操作
if body.startswith(b'update_product_cache:'):
product_id = body.split(b':')[1]
product_info = get_product_info_from_db(product_id)
r.set('product:' + product_id.decode('utf-8'), product_info)
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='cache_update')
channel.basic_consume(queue='cache_update', on_message_callback=callback, auto_ack=True)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
- 局部更新策略:
- 原理:当缓存数据发生变化时,尽量只更新变化的部分,而不是整个缓存。例如对于一个包含多个字段的用户缓存,当用户的手机号发生变化时,只更新手机号字段,而不是整个用户缓存。这样可以减少因一个缓存更新而引发其他相关缓存更新的连锁反应。
- 示例:在Redis中,如果用户缓存结构是哈希类型,使用
hset
命令更新单个字段:
r.hset('user:1', 'phone', 'new_phone_number')
而不是删除整个user:1
缓存后重新设置所有用户信息。