Redis备忘录
基础知识
缓存设计思想
缓存的主要目的是提高数据访问速度,减少后端数据库的压力。设计时需要考虑:
- 数据一致性:缓存与数据库中的数据需保持一致。
- 缓存失效策略:如LRU(最近最少使用)等,以便有效管理缓存中的数据。
- 数据过期:设置合理的过期时间,避免不必要的数据占用缓存空间。
缓存开发规范
在使用Redis时,应遵循一些开发规范:
- 键命名规则:应清晰且具有描述性,例如使用
user:1001:session
这样的格式。 - 异常处理:在操作Redis时应处理可能的异常情况,确保系统的健壮性。
- 性能监控:定期监控Redis的性能指标,如命中率、内存使用情况等。
缓存架构模式
常见的缓存架构模式包括:
- 本地缓存:将数据缓存到应用服务器内存中,适合小型应用。
- 集中式缓存:使用Redis作为集中式缓存,适合分布式系统。
- CDN缓存:在内容分发网络(CDN)中缓存静态内容,提高访问速度。
Redis概述
Redis是一个高性能的键值存储系统,支持多种数据结构(如字符串、哈希、列表、集合等)。它的特点包括:
- 内存存储:数据存储在内存中,访问速度极快。
- 持久化:支持RDB和AOF持久化机制,确保数据安全。
Redis性能原理
Redis的高性能来源于:
- 单线程模型:使用单线程处理请求,避免了上下文切换的开销。
- 非阻塞I/O:使用事件驱动架构,能够快速响应大量请求。
Redis安装和访问
Redis的安装相对简单,可以通过源码编译或包管理工具进行安装。访问Redis一般使用客户端命令行工具(redis-cli
)或通过编程语言的Redis客户端库。
Redis应用场景
Redis广泛应用于各种场景,包括但不限于:
- 缓存:减少数据库负担,提高查询速度。
- 会话存储:存储用户会话信息。
- 排行榜/计数器:利用Redis的有序集合实现实时排行榜。
实例
# 使用 Docker 启动 Redis
docker run --name redis -d -p 6379:6379 redis
使用 Shell 脚本与 Redis 交互
创建一个简单的 Shell 脚本来与 Redis 进行交互。
shell_script.sh
#!/bin/bash
# 设置 Redis 服务器地址
REDIS_HOST="localhost"
REDIS_PORT=6379
# 向 Redis 写入数据
echo "SET mykey 'Hello, Redis!'" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
# 从 Redis 读取数据
echo "GET mykey" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
运行 Shell 脚本
chmod +x shell_script.sh
./shell_script.sh
使用 Golang 访问 Redis
创建一个简单的 Go 应用程序,使用 go-redis
库连接 Redis 并进行操作。
main.go
package main
import (
"fmt"
"log"
"github.com/go-redis/redis/v8"
"context"
)
func main() {
ctx := context.Background()
// 创建 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 向 Redis 写入数据
err := rdb.Set(ctx, "mykey", "Hello, Redis!", 0).Err()
if err != nil {
log.Fatalf("Could not set key: %v", err)
}
// 从 Redis 读取数据
val, err := rdb.Get(ctx, "mykey").Result()
if err != nil {
log.Fatalf("Could not get key: %v", err)
}
fmt.Printf("mykey: %s\n", val)
}
运行 Golang 程序
go get github.com/go-redis/redis/v8
然后运行 Go 程序:
go run main.go
Redis 数据结构分析
Redis 数据结构简介
SDS 字符串
- SDS(Simple Dynamic Strings)是 Redis 中的字符串实现,处理字符串时避免了传统字符串的缺陷。
跳跃表
- 跳跃表用于实现有序集合,提供高效的插入、删除和查找操作。
压缩列表
- 压缩列表用于节省内存空间,适合存储小数据量的列表和哈希。
整数集合
- 整数集合是一种集合,实现了对整数的高效存储和操作。
字典
- Redis 使用字典(哈希表)来实现键值对存储。
quicklist
- quicklist 是一个混合数据结构,用于列表,结合了链表和压缩列表的优点。
Stream
- Stream 是 Redis 5.0 引入的一种新的数据结构,适合处理实时数据流。
HyperLogLog
- HyperLogLog 用于估算唯一元素的基数,节省内存。
RedisObject 结构
- RedisObject 结构用于表示 Redis 中的所有数据类型,包含元数据和实际数据。
使用 Shell 脚本与 Redis 交互
我们将创建一个 Shell 脚本,展示如何使用 Redis 不同的数据结构。
shell_script.sh
#!/bin/bash
# 设置 Redis 服务器地址
REDIS_HOST="localhost"
REDIS_PORT=6379
# 使用 SDS 存储字符串
echo "SET mystring 'Hello, Redis!'" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
# 使用集合存储数据
echo "SADD myset 'value1' 'value2' 'value3'" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
# 使用哈希存储数据
echo "HSET myhash field1 'Hello' field2 'World'" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
# 使用列表存储数据
echo "LPUSH mylist 'item1' 'item2' 'item3'" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
# 输出所有数据
echo "GET mystring" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
echo "SMEMBERS myset" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
echo "HGETALL myhash" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
echo "LRANGE mylist 0 -1" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
运行 Shell 脚本
chmod +x shell_script.sh
./shell_script.sh
使用 Golang 访问 Redis 数据结构
接下来,我们将创建一个 Go 程序,演示如何使用 Redis 的不同数据结构。
main.go
package main
import (
"context"
"fmt"
"log"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
// 创建 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 使用 SDS 存储字符串
err := rdb.Set(ctx, "mystring", "Hello, Redis!", 0).Err()
if err != nil {
log.Fatalf("Could not set string: %v", err)
}
// 使用集合存储数据
err = rdb.SAdd(ctx, "myset", "value1", "value2", "value3").Err()
if err != nil {
log.Fatalf("Could not add to set: %v", err)
}
// 使用哈希存储数据
err = rdb.HSet(ctx, "myhash", "field1", "Hello", "field2", "World").Err()
if err != nil {
log.Fatalf("Could not set hash: %v", err)
}
// 使用列表存储数据
err = rdb.LPush(ctx, "mylist", "item1", "item2", "item3").Err()
if err != nil {
log.Fatalf("Could not push to list: %v", err)
}
// 输出所有数据
val, err := rdb.Get(ctx, "mystring").Result()
if err != nil {
log.Fatalf("Could not get string: %v", err)
}
fmt.Printf("mystring: %s\n", val)
members, err := rdb.SMembers(ctx, "myset").Result()
if err != nil {
log.Fatalf("Could not get set members: %v", err)
}
fmt.Printf("myset: %v\n", members)
hash, err := rdb.HGetAll(ctx, "myhash").Result()
if err != nil {
log.Fatalf("Could not get hash: %v", err)
}
fmt.Printf("myhash: %v\n", hash)
list, err := rdb.LRange(ctx, "mylist", 0, -1).Result()
if err != nil {
log.Fatalf("Could not get list: %v", err)
}
fmt.Printf("mylist: %v\n", list)
}
运行 Golang 程序
然后运行 Go 程序:
go run main.go
Redis 工作机制分析
Redis 存储结构
Redis 使用一种基于字典的哈希表实现键值存储。键和值的存储结构都经过优化,以提高性能和响应速度。
键管理机制
Redis 通过哈希表来管理键,具有 O(1) 的平均时间复杂度进行插入、查找和删除操作。键的过期管理通过维护一个过期时间戳的跳表来实现。
持久化机制
Redis 提供两种主要的持久化机制:
- RDB(Redis Database Backup):周期性地将内存数据快照保存到磁盘。
- AOF(Append Only File):记录每一个写操作的日志,确保数据不丢失。
客户端创建和关闭机制
Redis 提供简单的连接机制,客户端可以通过 TCP 连接 Redis 服务器。连接关闭时,Redis 会释放相关资源。
命令请求处理流程
当 Redis 接收到命令请求时,首先会将其解析为命令类型,然后执行相应的操作。
服务器启动流程
Redis 服务器启动时,会加载持久化的数据,并进行必要的初始化工作。
事件处理
Redis 通过事件驱动机制处理客户端请求,使用 I/O 多路复用提高并发能力。
使用 Shell 脚本进行 Redis 操作
shell_script.sh
#!/bin/bash
# 设置 Redis 服务器地址
REDIS_HOST="localhost"
REDIS_PORT=6379
# 启动 Redis 服务器并设置持久化策略
echo "Starting Redis server..."
docker run --name redis -d -p 6379:6379 redis --appendonly yes
# 等待 Redis 服务器启动
sleep 5
# 添加一些数据
echo "Adding data to Redis..."
echo "SET mykey 'Hello, Redis!'" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
echo "SET anotherkey 'Persistent Data'" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
# 检查 AOF 文件
echo "Checking AOF file..."
docker exec redis ls /data
# 关闭 Redis 服务器
echo "Stopping Redis server..."
docker stop redis
docker rm redis
运行 Shell 脚本
chmod +x shell_script.sh
./shell_script.sh
使用 Golang 访问 Redis
main.go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
// 创建 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 添加数据到 Redis
err := rdb.Set(ctx, "mykey", "Hello, Redis!", 0).Err()
if err != nil {
log.Fatalf("Could not set key: %v", err)
}
err = rdb.Set(ctx, "anotherkey", "Persistent Data", 0).Err()
if err != nil {
log.Fatalf("Could not set key: %v", err)
}
// 读取数据
val, err := rdb.Get(ctx, "mykey").Result()
if err != nil {
log.Fatalf("Could not get key: %v", err)
}
fmt.Printf("mykey: %s\n", val)
// 检查 AOF 文件是否存在
aofFile := "/data/appendonly.aof"
if _, err := os.Stat(aofFile); os.IsNotExist(err) {
fmt.Printf("AOF file does not exist.\n")
} else {
fmt.Printf("AOF file exists: %s\n", aofFile)
}
// 关闭 Redis 客户端
rdb.Close()
}
运行 Golang 程序
go run main.go
Redis 高级特性原理
发布与订阅原理
Redis 的发布/订阅(Pub/Sub)模式允许消息的发送者(发布者)向多个接收者(订阅者)发送消息,适用于实时应用。
事务实现机制
Redis 支持基本的事务机制,通过 MULTI、EXEC、DISCARD 命令实现原子性操作。
慢查询日志
Redis 提供慢查询日志功能,可以帮助开发者识别性能瓶颈,优化 SQL 查询。
Lua 脚本编程
Redis 支持 Lua 脚本编程,允许用户在 Redis 服务器端执行复杂的操作,避免多个网络往返。
使用 Shell 脚本进行发布与订阅
我们将创建一个 Shell 脚本来演示 Redis 的发布与订阅功能。
pubsub_script.sh
#!/bin/bash
# 设置 Redis 服务器地址
REDIS_HOST="localhost"
REDIS_PORT=6379
# 启动 Redis 服务器
docker run --name redis -d -p 6379:6379 redis
# 启动发布者
echo "Starting publisher..."
(
sleep 1
for i in {1..5}
do
echo "PUBLISH mychannel "Message $i"" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
sleep 1
done
) &
# 启动订阅者
echo "Starting subscriber..."
redis-cli -h $REDIS_HOST -p $REDIS_PORT SUBSCRIBE mychannel
运行 Shell 脚本
chmod +x pubsub_script.sh
./pubsub_script.sh
使用 Golang 实现事务和慢查询日志
接下来,我们将创建一个 Go 程序,演示如何使用 Redis 的事务和慢查询日志功能。
main.go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
// 创建 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 开始事务
pipe := rdb.TxPipeline()
// 向事务中添加命令
pipe.Set(ctx, "key1", "value1", 0)
pipe.Set(ctx, "key2", "value2", 0)
// 执行事务
_, err := pipe.Exec(ctx)
if err != nil {
log.Fatalf("Transaction failed: %v", err)
}
fmt.Println("Transaction executed successfully.")
// 模拟慢查询
time.Sleep(2 * time.Second) // 模拟慢查询
// 记录慢查询
fmt.Println("Simulating slow query... (This is just a log, actual implementation requires configuration)")
// 读取数据
val1, err := rdb.Get(ctx, "key1").Result()
if err != nil {
log.Fatalf("Could not get key1: %v", err)
}
val2, err := rdb.Get(ctx, "key2").Result()
if err != nil {
log.Fatalf("Could not get key2: %v", err)
}
fmt.Printf("key1: %s, key2: %s\n", val1, val2)
// 关闭 Redis 客户端
rdb.Close()
}
运行 Golang 程序
go run main.go
使用 Lua 脚本进行操作
接下来,我们将展示如何使用 Lua 脚本在 Redis 中执行操作。
lua_script.lua
-- Lua 脚本:将两个键的值相加
local key1 = KEYS[1]
local key2 = KEYS[2]
local sum_key = KEYS[3]
local value1 = tonumber(redis.call('GET', key1))
local value2 = tonumber(redis.call('GET', key2))
if value1 and value2 then
redis.call('SET', sum_key, value1 + value2)
return value1 + value2
else
return nil
end
运行 Lua 脚本
我们可以通过 Redis CLI 运行 Lua 脚本:
redis-cli --eval lua_script.lua key1 key2 sum_key
Redis 集群高可用实践
主从复制
Redis 主从复制是通过将主节点的数据复制到从节点来实现的。这有助于数据冗余和负载均衡。
哨兵模式
Redis 哨兵模式用于监控主从节点,自动故障转移,并提供高可用性的服务。
Redis Cluster
Redis Cluster 是 Redis 官方提供的集群解决方案,支持数据分片和自动故障转移。
Codis 集群
Codis 是一个基于 Redis 的分布式框架,提供了更高层次的抽象,支持数据分片和高可用性。
使用 Shell 脚本搭建主从复制
master_slave_script.sh
#!/bin/bash
# 启动 Redis 主节点
echo "Starting Redis master..."
docker run --name redis-master -d -p 6379:6379 redis
# 启动 Redis 从节点
echo "Starting Redis slave..."
docker run --name redis-slave --link redis-master -d redis redis-server --slaveof redis-master 6379
# 等待主从节点启动
sleep 5
# 测试主从复制
echo "Testing master-slave replication..."
echo "SET key1 'Hello, Redis!'" | redis-cli -h localhost -p 6379
echo "GET key1 from master: $(redis-cli -h localhost -p 6379 GET key1)"
echo "GET key1 from slave: $(redis-cli -h redis-slave -p 6379 GET key1)"
# 停止并删除容器
echo "Stopping and removing containers..."
docker stop redis-master redis-slave
docker rm redis-master redis-slave
运行 Shell 脚本
chmod +x master_slave_script.sh
./master_slave_script.sh
使用 Golang 实现 Redis 哨兵模式
sentinel_example.go
package main
import (
"context"
"fmt"
"log"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
// 创建 Redis 哨兵客户端
rdb := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: "mymaster",
SentinelAddrs: []string{"localhost:26379"}, // 替换为您的哨兵地址
})
// 设置数据
err := rdb.Set(ctx, "key1", "Hello, Sentinel!", 0).Err()
if err != nil {
log.Fatalf("Could not set key: %v", err)
}
// 获取数据
val, err := rdb.Get(ctx, "key1").Result()
if err != nil {
log.Fatalf("Could not get key: %v", err)
}
fmt.Printf("key1: %s\n", val)
// 关闭 Redis 客户端
rdb.Close()
}
运行 Golang 程序
go run sentinel_example.go
使用 Redis Cluster
cluster_example.go
package main
import (
"context"
"fmt"
"log"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
// 创建 Redis Cluster 客户端
rdb := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{"localhost:7000", "localhost:7001", "localhost:7002"}, // 替换为您的集群节点地址
})
// 设置数据
err := rdb.Set(ctx, "key1", "Hello, Redis Cluster!", 0).Err()
if err != nil {
log.Fatalf("Could not set key: %v", err)
}
// 获取数据
val, err := rdb.Get(ctx, "key1").Result()
if err != nil {
log.Fatalf("Could not get key: %v", err)
}
fmt.Printf("key1: %s\n", val)
// 关闭 Redis Cluster 客户端
rdb.Close()
}
使用 Codis 集群
环境准备
确保您的环境中安装了以下工具:
- Docker
- Go(用于编写 Golang 示例)
安装 Codis
Codis 提供了 Docker 镜像,您可以使用 Docker 来快速启动 Codis 集群。
启动 Codis 集群
以下是一个简单的 Docker Compose 配置文件,用于启动 Codis 集群。
docker-compose.yml
version: '3'
services:
zookeeper:
image: zookeeper:3.4.6
ports:
- "2181:2181"
codis-dashboard:
image: codis-dashboard:latest
ports:
- "18087:18087"
environment:
- ZK_HOST=zookeeper:2181
codis-proxy:
image: codis-proxy:latest
ports:
- "19000:19000"
environment:
- ZK_HOST=zookeeper:2181
codis-server:
image: redis:latest
ports:
- "6379:6379"
启动服务
在同一目录下运行以下命令启动 Codis 集群:
docker-compose up -d
配置 Codis
访问 Codis Dashboard,通过浏览器打开 http://localhost:18087
,您可以在此配置 Codis 集群。
- 添加 Redis 实例;
- 配置分片;
- 启动 Proxy。
使用 Golang 连接 Codis 集群
main.go
package main
import (
"context"
"fmt"
"log"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
// 创建 Codis Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:19000", // Codis Proxy 地址
})
// 设置数据
err := rdb.Set(ctx, "key1", "Hello, Codis!", 0).Err()
if err != nil {
log.Fatalf("Could not set key: %v", err)
}
// 获取数据
val, err := rdb.Get(ctx, "key1").Result()
if err != nil {
log.Fatalf("Could not get key: %v", err)
}
fmt.Printf("key1: %s\n", val)
// 关闭 Redis 客户端
rdb.Close()
}
运行 Golang 示例
go run main.go
Redis 企业解决方案分析
缓存命中率
缓存命中率是衡量缓存性能的重要指标。提高缓存命中率可以显著提升系统性能。
缓存预热
缓存预热是指在系统启动时,提前将数据加载到缓存中,以提高缓存命中率。
缓存穿透
缓存穿透是指请求的数据在缓存和数据库中都不存在,导致每次请求都直接查询数据库,增加数据库压力。
缓存雪崩
缓存雪崩是指缓存中的大量数据在同一时间失效,导致大量请求直接打到数据库。
缓存击穿
缓存击穿是指某个热点数据在缓存中失效,导致大量请求同时打到数据库。
Hot Key 和 Big Key
- Hot Key:访问频率极高的键,会导致性能瓶颈。
- Big Key:存储大量数据的键,可能导致内存使用不当。
缓存更新策略
缓存更新策略涉及如何在数据变更时更新缓存,以保持数据一致性。
缓存与数据库一致性
确保缓存和数据库之间的数据一致性是关键问题之一。
分布式锁
分布式锁用于在分布式系统中控制资源访问,确保数据一致性。
缓存预热示例
我们将使用 Shell 脚本来预热缓存。
warm_cache.sh
#!/bin/bash
# 设置 Redis 服务器地址
REDIS_HOST="localhost"
REDIS_PORT=6379
# 预热缓存
echo "Prewarming cache..."
for i in {1..10}
do
echo "SET key$i 'Preheated Value $i'" | redis-cli -h $REDIS_HOST -p $REDIS_PORT
done
echo "Cache prewarming completed."
运行 Shell 脚本
chmod +x warm_cache.sh
./warm_cache.sh
缓存穿透示例
cache_penetration.go
package main
import (
"context"
"fmt"
"log"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 模拟查询数据库
key := "nonexistent_key"
val, err := rdb.Get(ctx, key).Result()
if err == redis.Nil {
// 缓存穿透,直接查询数据库
fmt.Printf("Cache miss for key: %s. Querying database...\n", key)
// 这里可以模拟数据库查询
val = queryDatabase(key)
} else if err != nil {
log.Fatalf("Could not get key: %v", err)
} else {
fmt.Printf("Cache hit for key: %s, value: %s\n", key, val)
}
}
func queryDatabase(key string) string {
// 模拟数据库查询
return "Database Value"
}
运行 Golang 程序
go run cache_penetration.go
缓存雪崩及击穿示例
我们将使用 Golang 模拟缓存雪崩和击穿。
avalanche_and_thunder.go
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
var rdb *redis.Client
func init() {
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
}
func getData(key string) string {
val, err := rdb.Get(ctx, key).Result()
if err == redis.Nil {
fmt.Printf("Cache miss for key: %s\n", key)
// 模拟缓存击穿
val = queryDatabase(key)
go func() {
time.Sleep(1 * time.Second) // 假设数据在缓存中1秒后失效
rdb.Set(ctx, key, val, 0)
}()
}
return val
}
func queryDatabase(key string) string {
// 模拟数据库延迟
time.Sleep(2 * time.Second)
return "Database Value"
}
func main() {
var wg sync.WaitGroup
keys := []string{"key1", "key2", "key3"}
for _, key := range keys {
wg.Add(1)
go func(k string) {
defer wg.Done()
fmt.Println(getData(k))
}(key)
}
wg.Wait()
}
运行 Golang 程序
go run avalanche_and_thunder.go
分布式锁示例
distributed_lock.go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
lockKey := "my_lock"
lockValue := "locked"
// 尝试获取锁
ok, err := rdb.SetNX(ctx, lockKey, lockValue, 5*time.Second).Result()
if err != nil {
log.Fatalf("Could not acquire lock: %v", err)
}
if !ok {
fmt.Println("Could not acquire lock, another process holds it.")
return
}
fmt.Println("Lock acquired. Performing some operations...")
// 模拟操作
time.Sleep(3 * time.Second)
// 释放锁
rdb.Del(ctx, lockKey)
fmt.Println("Lock released.")
}
运行 Golang 程序
go run distributed_lock.go