Redis在缓存设计中的应用与优化
2023-06-211.2k 阅读
Redis基础概述
Redis简介
Redis 是一个开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。Redis 支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)等。这些丰富的数据结构使得 Redis 在不同场景下都能展现出强大的功能,尤其是在缓存设计中,能够满足各种复杂的缓存需求。
Redis数据结构特点与缓存应用优势
- 字符串(String)
- 特点:最基本的数据结构,一个 key 对应一个 value。value 不仅可以是字符串,还可以是数字等类型。
- 缓存应用优势:常用于缓存简单的对象,比如用户的基本信息(用户名、头像地址等)。在 Web 应用中,对于一些不经常变化且访问频繁的数据,使用字符串结构缓存可以快速响应请求,减少数据库的负载。例如,缓存一篇博客文章的标题和简短摘要。
- 代码示例(以 Python 为例):
import redis
r = redis.Redis(host='localhost', port=6379, db = 0)
r.set('blog:1:title', 'Redis 缓存设计实战')
title = r.get('blog:1:title')
print(title.decode('utf - 8'))
- 哈希(Hash)
- 特点:哈希表结构,一个 key 对应多个 field - value 对。适用于存储对象的多个属性。
- 缓存应用优势:在缓存复杂对象时非常有用,比如缓存用户完整信息(包括姓名、年龄、性别、联系方式等多个属性)。相比于字符串结构,哈希结构在存储和读取对象的部分属性时更高效,并且不会因为修改一个属性而导致整个对象缓存的更新。
- 代码示例(以 Python 为例):
user_info = {
'name': 'John',
'age': 30,
'gender': 'Male'
}
r.hmset('user:1', user_info)
name = r.hget('user:1', 'name')
print(name.decode('utf - 8'))
- 列表(List)
- 特点:简单的字符串列表,按照插入顺序排序。支持在列表的两端进行插入和删除操作。
- 缓存应用优势:可以用于缓存一些有序的数据,比如文章的评论列表。通过列表的两端操作,可以方便地实现最新评论的插入和获取。还可以用于实现简单的消息队列,将消息插入到列表一端,另一端进行消费。
- 代码示例(以 Python 为例):
comments = ['Great article!', 'Interesting topic']
for comment in comments:
r.rpush('blog:1:comments', comment)
first_comment = r.lpop('blog:1:comments')
print(first_comment.decode('utf - 8'))
- 集合(Set)
- 特点:无序的字符串集合,集合中的每个元素都是唯一的。支持集合的交、并、差等操作。
- 缓存应用优势:在缓存一些需要去重的数据时非常合适,比如网站的访客 IP 集合。可以方便地统计独立访客数量,并且通过集合操作可以实现一些复杂的功能,如找出同时访问了多个页面的用户。
- 代码示例(以 Python 为例):
ips = ['192.168.1.1', '10.0.0.1', '192.168.1.1']
for ip in ips:
r.sadd('website:visitors', ip)
count = r.scard('website:visitors')
print(count)
- 有序集合(Sorted Set)
- 特点:和集合类似,但每个元素都会关联一个分数(score),通过分数来对元素进行排序。
- 缓存应用优势:常用于排行榜类的场景,比如游戏玩家的积分排行榜。根据分数的变化,可以实时更新排行榜,并且高效地获取排名靠前或特定范围内的玩家信息。
- 代码示例(以 Python 为例):
players = {
'Player1': 1000,
'Player2': 800,
'Player3': 1200
}
for player, score in players.items():
r.zadd('game:leaderboard', {player: score})
top_players = r.zrevrange('game:leaderboard', 0, 2, withscores=True)
for player, score in top_players:
print(player.decode('utf - 8'), score)
Redis在缓存设计中的应用场景
页面缓存
在 Web 应用中,页面缓存是 Redis 常见的应用场景之一。对于一些不经常变化的页面,比如公司的介绍页面、产品的静态展示页面等,可以将整个页面缓存到 Redis 中。当用户请求这些页面时,首先检查 Redis 中是否存在对应的缓存页面,如果存在则直接返回,避免了重复的页面渲染和数据库查询操作。
- 实现原理:在服务器端,当页面第一次被请求时,生成页面内容并将其存储到 Redis 中,设置一个合适的过期时间。后续请求该页面时,先从 Redis 中获取缓存内容,如果缓存存在且未过期,则直接返回给用户;否则重新生成页面并更新缓存。
- 代码示例(以 Java Spring Boot 为例):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class PageCacheService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public String getCachedPage(String pageKey) {
return redisTemplate.opsForValue().get(pageKey);
}
public void cachePage(String pageKey, String pageContent, long expirationTime) {
redisTemplate.opsForValue().set(pageKey, pageContent, expirationTime);
}
}
在控制器中使用:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PageController {
@Autowired
private PageCacheService pageCacheService;
@GetMapping("/page/{pageKey}")
public String getPage(@PathVariable String pageKey) {
String cachedPage = pageCacheService.getCachedPage(pageKey);
if (cachedPage!= null) {
return cachedPage;
}
// 如果缓存不存在,生成页面内容
String pageContent = generatePageContent(pageKey);
pageCacheService.cachePage(pageKey, pageContent, 3600); // 缓存1小时
return pageContent;
}
private String generatePageContent(String pageKey) {
// 实际生成页面内容的逻辑
return "This is the content of page " + pageKey;
}
}
数据缓存
- 单对象缓存
- 应用场景:在数据库中有很多单条记录的数据,比如用户信息、商品信息等,这些数据在系统中经常被读取,且更新频率相对较低。将这些单条记录缓存到 Redis 中,可以显著提高读取性能。
- 实现方式:以对象的唯一标识(如用户 ID、商品 ID)作为 Redis 的 key,对象的序列化数据作为 value 进行存储。当需要获取对象时,先从 Redis 中查询,如果存在则直接返回;如果不存在则从数据库中读取,然后将读取到的对象存入 Redis 缓存。
- 代码示例(以 Node.js 为例):
const redis = require('redis');
const { promisify } = require('util');
const client = redis.createClient();
async function getSingleObject(id) {
let obj = await promisify(client.get).bind(client)(`object:${id}`);
if (obj) {
return JSON.parse(obj);
}
// 从数据库中获取对象
const dbObj = await getObjectFromDatabase(id);
if (dbObj) {
await promisify(client.set).bind(client)(`object:${id}`, JSON.stringify(dbObj));
return dbObj;
}
return null;
}
async function getObjectFromDatabase(id) {
// 实际从数据库获取对象的逻辑
return { id, name: 'Sample Object' };
}
- 列表数据缓存
- 应用场景:对于一些列表数据,如文章列表、订单列表等,缓存这些列表可以减少数据库的分页查询压力。尤其是在列表数据变化不频繁的情况下,缓存的效果更为显著。
- 实现方式:可以将整个列表数据序列化后存储到 Redis 中,也可以根据分页情况,将不同页的列表数据分别缓存。当请求列表数据时,先从 Redis 中获取,如果缓存存在则直接返回;否则从数据库中查询,更新缓存后再返回。
- 代码示例(以 C# 为例):
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Text.Json;
class ListCacheService
{
private readonly ConnectionMultiplexer _redis;
private readonly IDatabase _db;
public ListCacheService()
{
_redis = ConnectionMultiplexer.Connect("localhost:6379");
_db = _redis.GetDatabase();
}
public async Task<List<T>> GetList<T>(string key)
{
var cachedValue = await _db.StringGetAsync(key);
if (!cachedValue.IsNullOrEmpty)
{
return JsonSerializer.Deserialize<List<T>>(cachedValue);
}
// 从数据库获取列表数据
var list = await GetListFromDatabase<T>();
if (list!= null)
{
await _db.StringSetAsync(key, JsonSerializer.Serialize(list));
return list;
}
return null;
}
private async Task<List<T>> GetListFromDatabase<T>()
{
// 实际从数据库获取列表数据的逻辑
return new List<T>();
}
}
缓存穿透、缓存雪崩与缓存击穿问题及解决
- 缓存穿透
- 问题描述:缓存穿透是指查询一个一定不存在的数据,由于缓存中没有,每次都会去数据库查询,导致数据库压力增大。例如,恶意用户不断请求一个不存在的商品 ID。
- 解决方法:
- 布隆过滤器(Bloom Filter):在查询数据前,先通过布隆过滤器判断数据是否存在。布隆过滤器是一种概率型数据结构,它可以快速判断一个元素是否在集合中。如果布隆过滤器判断数据不存在,那么可以直接返回,不需要查询数据库。虽然布隆过滤器存在一定的误判率,但可以通过调整参数来控制误判率在可接受范围内。
- 空值缓存:当查询数据库发现数据不存在时,也将空值缓存到 Redis 中,并设置一个较短的过期时间,这样后续相同的查询可以直接从缓存中获取空值,避免多次查询数据库。
- 代码示例(以 Go 语言使用布隆过滤器为例):
package main
import (
"github.com/willf/bloom"
"fmt"
"github.com/go - redis/redis/v8"
"context"
)
var ctx = context.Background()
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
// 初始化布隆过滤器
bf := bloom.New(1000, 0.01)
// 假设已经将所有存在的商品 ID 添加到布隆过滤器中
bf.Add([]byte("1"))
bf.Add([]byte("2"))
productID := "3"
if!bf.Test([]byte(productID)) {
fmt.Println("Product does not exist, no need to query database")
return
}
// 如果布隆过滤器判断可能存在,再查询 Redis 和数据库
result, err := rdb.Get(ctx, productID).Result()
if err == nil {
fmt.Println("Product from cache:", result)
} else {
// 查询数据库
product := getProductFromDB(productID)
if product!= nil {
rdb.Set(ctx, productID, product, 0)
fmt.Println("Product from database and cached:", product)
} else {
// 空值缓存
rdb.Set(ctx, productID, "", 60)
fmt.Println("Product does not exist, cached null value")
}
}
}
func getProductFromDB(id string) string {
// 实际从数据库获取商品的逻辑
return ""
}
- 缓存雪崩
- 问题描述:缓存雪崩是指在同一时间大量的缓存失效,导致大量请求直接落到数据库上,造成数据库压力过大甚至崩溃。通常是因为设置了相同的过期时间,例如一批缓存都设置了 1 小时过期,1 小时后这些缓存同时失效。
- 解决方法:
- 随机过期时间:为每个缓存设置一个随机的过期时间,避免大量缓存同时过期。例如,将过期时间设置为 1 小时到 1 小时 30 分钟之间的随机值。
- 加锁排队:当缓存失效时,先通过加锁机制,只允许一个请求去查询数据库并更新缓存,其他请求等待。这样可以避免大量请求同时查询数据库。
- 代码示例(以 Python 实现随机过期时间为例):
import redis
import random
r = redis.Redis(host='localhost', port=6379, db = 0)
def setWithRandomExpiry(key, value):
min_expiry = 3600
max_expiry = 5400
expiry = random.randint(min_expiry, max_expiry)
r.setex(key, expiry, value)
- 缓存击穿
- 问题描述:缓存击穿是指一个热点 key,在缓存过期的瞬间,大量请求同时访问,导致这些请求都直接落到数据库上。例如,一个热门商品的缓存过期时,大量用户同时请求该商品信息。
- 解决方法:
- 互斥锁:在缓存失效时,使用互斥锁(如 Redis 的 SETNX 命令)来保证只有一个请求可以去查询数据库并更新缓存,其他请求等待。当获取到锁的请求更新完缓存后,释放锁,其他请求可以从缓存中获取数据。
- 永不过期:对于热点数据,可以设置缓存永不过期,同时在更新数据库时,主动更新缓存,这样可以避免缓存过期瞬间的高并发问题。
- 代码示例(以 Java 使用互斥锁解决缓存击穿为例):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CacheBreakdownService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public String getValue(String key) {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
String lockKey = "lock:" + key;
boolean locked = false;
try {
locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 1, TimeUnit.MINUTES);
if (locked) {
// 从数据库获取值
value = getValueFromDatabase(key);
if (value!= null) {
redisTemplate.opsForValue().set(key, value);
}
} else {
// 等待一段时间后重试
Thread.sleep(100);
return getValue(key);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (locked) {
redisTemplate.delete(lockKey);
}
}
}
return value;
}
private String getValueFromDatabase(String key) {
// 实际从数据库获取值的逻辑
return "Sample value from database";
}
}
Redis缓存优化策略
缓存数据结构选择优化
- 根据访问模式选择
- 读多写少:如果数据主要是读操作,且对读取性能要求极高,对于简单数据可以优先选择字符串结构。例如,对于一些配置信息,如网站的版权声明、客服电话等,使用字符串结构缓存可以快速读取。对于复杂对象,哈希结构是较好的选择,因为它可以高效地读取对象的部分属性。
- 读写均衡:当读写操作频率较为均衡时,需要综合考虑数据结构的特点。对于有序数据,如按时间排序的日志记录,有序集合可能更合适,既能方便地插入新记录,又能高效地按时间范围查询。对于无序且需要去重的数据,集合结构是不错的选择。
- 写多读少:在写多读少的场景下,要考虑数据结构的写入性能。列表结构在两端插入操作上性能较好,适合用于消息队列等场景,快速将消息写入队列。而哈希结构在更新对象部分属性时也比较高效,不需要更新整个对象。
- 根据数据规模选择
- 小数据量:对于小数据量的缓存,各种数据结构的性能差异并不明显,此时可以根据数据的逻辑结构来选择。例如,如果是单个简单值,字符串结构即可;如果是多个相关属性的对象,哈希结构更合适。
- 大数据量:当缓存数据量较大时,需要考虑数据结构的内存占用和操作性能。对于非常大的列表数据,可能需要考虑分页存储或者使用更适合大数据量的存储方式。有序集合在大数据量下,如果频繁进行插入和删除操作,可能会影响性能,需要根据实际情况进行优化。
缓存过期策略优化
- 精确过期时间设置
- 基于业务逻辑:根据业务数据的更新频率来精确设置过期时间。例如,对于实时性要求不高的新闻资讯,缓存过期时间可以设置为几个小时甚至一天;而对于实时的股票价格数据,过期时间可能设置为几分钟甚至更短。
- 动态调整:可以根据数据的访问频率动态调整过期时间。对于访问频繁的数据,适当延长过期时间;对于长时间未被访问的数据,提前过期以释放内存。
- 代码示例(以 Python 动态调整过期时间为例):
import redis
r = redis.Redis(host='localhost', port=6379, db = 0)
def getValueWithDynamicExpiry(key):
value = r.get(key)
if value:
# 增加访问计数
visitCountKey = f'{key}:visit_count'
r.incr(visitCountKey)
count = int(r.get(visitCountKey))
if count % 10 == 0:
# 每访问10次,延长过期时间
r.expire(key, 7200)
return value.decode('utf - 8')
else:
# 从数据库获取值
value = getValueFromDatabase(key)
if value:
r.setex(key, 3600, value)
r.setex(visitCountKey, 3600, 1)
return value
return None
def getValueFromDatabase(key):
# 实际从数据库获取值的逻辑
return 'Sample value from database'
- 缓存预热
- 原理:缓存预热是指在系统上线前或者在业务高峰来临前,提前将一些热点数据加载到缓存中,避免在业务运行时由于缓存未命中导致大量请求直接访问数据库。
- 实现方式:可以通过脚本在系统启动时,从数据库中读取热点数据并写入 Redis 缓存。也可以根据历史数据和业务预测,定期更新缓存中的预热数据。
- 代码示例(以 Shell 脚本实现缓存预热为例):
#!/bin/bash
redis-cli -h localhost -p 6379 del "product:1"
redis-cli -h localhost -p 6379 set "product:1" "$(mysql -u root -p -D mydb -e "SELECT * FROM products WHERE id = 1" | tail -n +2)"
redis-cli -h localhost -p 6379 del "user:1"
redis-cli -h localhost -p 6379 set "user:1" "$(mysql -u root -p -D mydb -e "SELECT * FROM users WHERE id = 1" | tail -n +2)"
Redis配置优化
- 内存配置
- 合理分配内存:根据服务器的物理内存和应用需求,合理设置 Redis 的最大内存。可以通过
maxmemory
配置项来设置,例如设置为服务器物理内存的 70% 到 80%,避免 Redis 占用过多内存导致系统内存不足。 - 内存淘汰策略:选择合适的内存淘汰策略,如
volatile - lru
(在设置了过期时间的键中使用 LRU 算法淘汰键)、allkeys - lru
(在所有键中使用 LRU 算法淘汰键)、volatile - ttl
(淘汰即将过期的键)等。根据业务数据的特点和访问模式来选择,例如如果希望优先淘汰过期时间短的键,可以选择volatile - ttl
策略。
- 合理分配内存:根据服务器的物理内存和应用需求,合理设置 Redis 的最大内存。可以通过
- 网络配置
- 绑定地址:通过
bind
配置项绑定 Redis 服务器监听的 IP 地址。如果只允许本地访问,可以绑定到127.0.0.1
;如果需要对外提供服务,需要绑定到服务器的公网 IP 或者0.0.0.0
(注意安全性)。 - 端口:默认端口是
6379
,可以根据需要修改为其他未被占用的端口,以提高安全性或者适应特定的网络环境。 - TCP 配置:调整 TCP 相关配置,如
tcp - keepalive
,设置为合适的值(如 60 秒),可以及时检测到客户端的异常断开,释放相关资源。
- 绑定地址:通过
缓存架构优化
- 主从复制
- 原理:主从复制是 Redis 提供的一种数据备份和读写分离机制。主节点负责处理写操作,并将写操作同步到从节点。从节点可以处理读操作,从而分担主节点的读压力。
- 配置方法:在从节点的配置文件中,通过
slaveof
配置项指定主节点的 IP 和端口。例如slaveof <master - ip> <master - port>
。主节点不需要额外配置,只需要正常启动。 - 优势:提高系统的读性能和可用性。当主节点出现故障时,从节点可以提升为主节点继续提供服务,保证系统的正常运行。
- 哨兵模式
- 原理:哨兵模式是在主从复制的基础上,增加了对主节点的监控和自动故障转移功能。哨兵节点会定期检查主节点和从节点的状态,当主节点出现故障时,哨兵节点会自动选举一个从节点提升为主节点,并通知其他从节点和客户端。
- 配置方法:需要配置多个哨兵节点,在哨兵节点的配置文件中,通过
sentinel monitor
配置项指定要监控的主节点信息,如sentinel monitor mymaster <master - ip> <master - port> <quorum>
,其中<quorum>
表示判断主节点失效至少需要的哨兵节点数。 - 优势:进一步提高了系统的可用性和稳定性,实现了主节点故障的自动处理,减少了人工干预。
- 集群模式
- 原理:Redis 集群是一种分布式架构,它将数据分布在多个节点上,每个节点负责一部分数据的存储和读写。集群通过哈希槽(hash slot)来分配数据,共有 16384 个哈希槽,每个键通过 CRC16 算法计算出哈希值,再对 16384 取模,得到对应的哈希槽,从而确定该键应该存储在哪个节点上。
- 配置方法:通过
redis - trib.rb
工具来创建和管理集群。例如,redis - trib.rb create --replicas 1 <node1 - ip>:<port1> <node2 - ip>:<port2>...
可以创建一个包含多个主节点和从节点的集群,其中--replicas 1
表示每个主节点有一个从节点。 - 优势:支持水平扩展,可以通过增加节点来提高系统的存储容量和处理能力。同时,集群模式也具备一定的容错能力,部分节点故障时,系统仍能正常运行。
通过对 Redis 在缓存设计中的应用和优化策略的深入理解与实践,可以构建出高性能、高可用且稳定的缓存系统,有效提升后端应用的整体性能和用户体验。在实际应用中,需要根据具体的业务场景和需求,灵活选择和组合各种优化方法,以达到最佳的缓存效果。