Redis高级用法以及golang代码示例
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">前言</a></li><li><a href="#_label1">一、Redis高性能基础</a></li><li><a href="#_label2">二、Redis使用场景</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_0">2.1 数据库缓存</a></li><li><a href="#_lab2_2_1">2.2 分布式缓存</a></li><li><a href="#_lab2_2_2">2.3 MQ消息中间件</a></li><li><a href="#_lab2_2_3">2.4 分布式锁</a></li><ul class="third_class_ul"><li><a href="#_label3_2_3_0">实践中的注意事项</a></li></ul></ul><li><a href="#_label3">三、代码示例</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_4">3.1 数据库缓存</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_3_5">3.2 分布式锁</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_3_6">3.3 消息队列 (Pub/Sub 和基于 List)</a></li><ul class="third_class_ul"><li><a href="#_label3_3_6_1">3.3.1 发布/订阅模式 (Pub/Sub)</a></li><li><a href="#_label3_3_6_2">3.3.2 基于 List 的队列</a></li></ul><li><a href="#_lab2_3_7">3.4 实践总结与建议</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label4">总结 </a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>前言</h2><p>Redis作为内存数据库在服务器使用非常广泛,除了基本的key-value缓存作为db的内存缓存用,还有一些高级feature,比如</p>
<ul><li>分布式缓存</li></ul>
<p>不同服务器节点节点间同步数据,redis可以是一个集群,缓存数据在多个服务器节点之间同步,比如同步session信息等。</p>
<ul><li>MQ</li></ul>
<p>Redis中的队列等机制可以实现订阅和发布机制,来实现事件通知的作用。</p>
<ul><li>分布式锁</li></ul>
<p>Redis可以实现分布式锁,用于分布式架构中共享资源的互斥访问。</p>
<table><thead><tr><th>角色定位</th><th>核心要解决的问题</th><th>Redis如何应对(核心机制)</th><th>典型使用场景</th></tr></thead><tbody><tr><td><strong>数据库缓存</strong></td><td>缓解后端数据库(如MySQL)的读取压力,提升应用响应速度。</td><td>通过内存存储实现极速读写;设置合理的过期时间(TTL)和淘汰策略。</td><td>缓存热点数据(如用户信息、商品详情)。</td></tr><tr><td><strong>分布式缓存</strong></td><td>在分布式系统中,为多个应用实例提供统一的缓存服务,实现数据共享。</td><td>通过<strong>Redis集群</strong>实现数据分片(Sharding)和高可用(High Availability)。</td><td>分布式Session存储、全局计数器。</td></tr><tr><td><strong>消息中间件</strong></td><td>实现应用组件间的异步通信和解耦。</td><td>使用<strong>发布订阅(Pub/Sub)</strong> 模式或<strong>List结构</strong>模拟消息队列。</td><td>实时消息通知、事件广播、简单的任务队列。</td></tr><tr><td><strong>分布式锁</strong></td><td>在分布式环境中,保证对共享资源的互斥访问。</td><td>利用 <code>SET</code>命令的 <code>NX</code>(不存在才设置)和 <code>PX</code>(过期时间)参数实现原子性加锁。</td><td>防止超卖、保证计划任务的单机执行。</td></tr></tbody></table>
<p class="maodian"><a name="_label1"></a></p><h2>一、Redis高性能基础</h2>
<p>要深入理解上述表格中的各种能力,我们需要探究Redis背后的设计哲学,这主要归结于两点:<strong>内存存储</strong>和<strong>单线程模型</strong>。</p>
<ol><li><p><strong>内存存储</strong></p>
<p>Redis将所有数据直接存放在内存中,这使得它的数据读写操作避免了传统磁盘数据库的I/O瓶颈,速度极快,通常能达到微秒级别的延迟。为了应对内存有限和进程重启导致数据丢失的问题,Redis提供了两种持久化机制:</p>
<ul><li><strong>RDB</strong>:在特定时间点创建整个数据集的快照。它是一个紧凑的二进制文件,非常适合备份和灾难恢复,但可能会丢失最后一次快照之后的数据。</li><li><strong>AOF</strong>:记录每一个写操作命令,以追加的方式写入日志文件。数据完整性更高,故障恢复时通过重放命令来重建状态,但文件体积通常更大,恢复速度较慢。在实际生产中,通常会结合使用两者。</li></ul></li><li><p><strong>单线程模型</strong></p>
<p>单线程如何应对高并发?这里的“单线程”指的是<strong>处理网络I/O和执行命令</strong>的核心模块是单线程的。这样做带来了巨大优势:</p>
<ul><li><strong>避免了多线程的锁竞争</strong>:所有命令串行执行,天然保证了原子性,无需担心并发安全问题。</li><li><strong>高效的I/O多路复用</strong>:Redis使用epoll这样的机制,用一个线程监控大量的客户端连接,只有在连接真正可读或可写时才会进行处理,极大地提升了CPU利用率。</li></ul>
<p>需要注意的是,Redis 6.0之后引入了多线程来处理网络I/O(例如数据的读取和发送),但命令的执行本身仍然是单线程的,从而保持了简单可靠的优势。</p></li></ol>
<p class="maodian"><a name="_label2"></a></p><h2>二、Redis使用场景</h2>
<p class="maodian"><a name="_lab2_2_0"></a></p><h3>2.1 数据库缓存</h3>
<p>这是Redis最经典的用法,其工作流程如下图所示:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/20261495307628.png" /></p>
<p>在实践中,需要注意两个经典问题:</p>
<ul><li><strong>缓存穿透</strong>:大量请求查询一个数据库中也不存在的数据,导致请求直接打到数据库。解决方案是使用<strong>布隆过滤器</strong>进行初步校验,或者将空值也缓存一小段时间。</li><li><strong>缓存雪崩</strong>:大量缓存数据在同一时间点过期,导致所有请求涌向数据库。解决方案是<strong>给缓存过期时间加上随机值</strong>,避免同时失效。</li></ul>
<p class="maodian"><a name="_lab2_2_1"></a></p><h3>2.2 分布式缓存</h3>
<p>当数据量巨大或并发极高时,单机Redis会成为瓶颈,或者多个分布式节点之间同步数据时,这时就需要<strong>Redis集群</strong>(Redis Cluster)。</p>
<ul><li><strong>数据分片</strong>:Redis集群将整个数据集划分为16384个<strong>哈希槽</strong>。每个键通过<code>CRC16</code>算法计算后,再对16384取模,确定其所属的槽。集群中的每个主节点负责一部分槽区,这样数据就被自动分布到了多个节点上。</li><li><strong>高可用</strong>:每个主节点都可以配置一个或多个从节点。当主节点故障时,集群会通过共识机制,自动将其下的某个从节点提升为新的主节点,继续提供服务,从而实现故障自动转移。</li></ul>
<p class="maodian"><a name="_lab2_2_2"></a></p><h3>2.3 MQ消息中间件</h3>
<p>Redis可通过两种方式实现消息传递:</p>
<ul><li><strong>发布订阅</strong>:发布者将消息发送到特定频道,所有订阅了该频道的订阅者都会即时收到消息。这是一种<strong>广播</strong>模式,但消息是<strong>非持久化</strong>的,即如果没有订阅者在线,消息就丢失了。</li><li><strong>List结构</strong>:使用<code>LPUSH</code>生产消息,<code>BRPOP</code>阻塞地消费消息。这种方式消息可以持久化,但一个消息只能被一个消费者消费,更适合于简单的点对点或任务队列场景。</li></ul>
<p class="maodian"><a name="_lab2_2_3"></a></p><h3>2.4 分布式锁</h3>
<p>在分布式系统中,协调多个进程对共享资源的访问,需要分布式锁。Redis因其原子操作和高性能成为常见选择。</p>
<p>核心命令是:</p>
<div class="jb51code"><pre class="brush:go;">SET lock_key unique_value NX PX 30000
</pre></div>
<ul><li><p><code>NX</code>:仅当键不存在时才设置,保证只有一个客户端能设置成功,即抢到锁。</p></li><li><p><code>PX 30000</code>:设置键的过期时间为30秒,防止客户端崩溃后锁无法释放,导致死锁。</p>
<p>锁的释放需要先检查值是否为当前客户端设置的<code>unique_value</code>,再执行删除,推荐使用Lua脚本保证这两个操作的原子性。对于更高要求的场景,可以考虑Redlock算法。</p></li></ul>
<p class="maodian"><a name="_label3_2_3_0"></a></p><h4>实践中的注意事项</h4>
<ul><li><strong>警惕大Key和热Key</strong>:避免单个Key的Value过大(如超过10KB),这会阻塞主线程。同时,要避免某个Key被极高频率地访问,成为瓶颈。需要做好监控和拆分。</li><li><strong>确保数据一致性</strong>:当缓存中的数据需要更新时,通常采用<strong>先更新数据库,再删除缓存</strong>的策略。这种方式相对简单且发生不一致的概率较低,但并非绝对,需要根据业务场景权衡。</li></ul>
<p class="maodian"><a name="_label3"></a></p><h2>三、代码示例</h2>
<p>了解完 Redis 的核心作用和工作原理后,下面用 Go 语言详细说明 Redis 在几种常见场景下的应用代码,</p>
<p>下表概括了这几种场景的核心实现方式和要点。</p>
<table><thead><tr><th>应用场景</th><th>核心 Go 代码示例 (github.com/go-redis/redis/v8)</th><th>关键实现要点</th></tr></thead><tbody><tr><td><strong>数据库缓存</strong></td><td><code>client.Set(ctx, key, value, expiration)</code> <code>client.Get(ctx, key)</code></td><td>1. <strong>缓存模式</strong>:先查缓存,未命中再查数据库。 2. <strong>缓存过期</strong>:务必设置 TTL。 3. <strong>序列化</strong>:复杂数据需 JSON 序列化。</td></tr><tr><td><strong>分布式锁</strong></td><td><code>client.SetNX(ctx, lockKey, value, expire)</code> + Lua 脚本解锁</td><td>1. <strong>原子加锁</strong>:使用 <code>SETNX</code>(或 <code>SET key value NX PX timeout</code>)。 2. <strong>安全释放</strong>:验证锁持有者(Lua 脚本保证原子性)。 3. <strong>自动续期</strong>:考虑"看门狗"机制避免业务超时。</td></tr><tr><td><strong>消息队列 (Pub/Sub)</strong></td><td><code>client.Publish(ctx, channel, message)</code> <code>client.Subscribe(ctx, channel...)</code></td><td>1. <strong>发布/订阅模式</strong>:轻量级广播消息,但无持久化。 2. <strong>消息丢失</strong>:注意网络断开可能导致消息丢失。</td></tr><tr><td><strong>消息队列 (List-based)</strong></td><td><code>client.LPush(ctx, queue, message)</code> <code>client.BRPop(ctx, timeout, queue)</code></td><td>1. <strong>点对点队列</strong>:基于 List 的 <code>LPUSH</code>/<code>BRPOP</code>。 2. <strong>阻塞消费</strong>:<code>BRPOP</code>避免轮询,节省资源。 3. <strong>消息持久化</strong>:消息在 Redis 中可持久化。</td></tr></tbody></table>
<p>下面我们来看具体的代码实现细节和需要注意的事项。</p>
<p class="maodian"><a name="_lab2_3_4"></a></p><h3>3.1 数据库缓存</h3>
<p>作为缓存是 Redis 最经典的用法。其核心流程是:<strong>收到请求时,先尝试从 Redis 中获取数据,如果命中则直接返回;如果未命中,则从底层数据库(如 MySQL)查询,并将结果写入 Redis 并设置过期时间,以便后续请求能直接从缓存中读取</strong>。</p>
<p>以下是一个完整的 Go 示例,包含了连接 Redis 和缓存查询的逻辑。</p>
<div class="jb51code"><pre class="brush:go;">package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/go-redis/redis/v8"
)
// 初始化Redis客户端
func InitRedisClient() *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis服务器地址
Password: "", // 密码,没有则为空
DB: 0, // 使用默认DB
})
// 通过Ping命令测试连接
ctx := context.Background()
_, err := rdb.Ping(ctx).Result()
if err != nil {
log.Fatalf("无法连接到Redis: %v", err)
}
fmt.Println("Redis连接成功")
return rdb
}
// 获取数据(缓存优先)
func GetData(rdb *redis.Client, key string) (string, error) {
ctx := context.Background()
// 1. 首先尝试从缓存获取
val, err := rdb.Get(ctx, key).Result()
if err == nil {
fmt.Println("缓存命中")
return val, nil // 成功命中,直接返回
}
if err != redis.Nil {
return "", err // 出现其他错误
}
// 2. 缓存未命中,从数据源(如数据库)获取
fmt.Println("缓存未命中,从数据源获取")
data, err := GetDataFromSource(key)
if err != nil {
return "", err
}
// 3. 将数据存入缓存,并设置过期时间(例如5分钟)
err = rdb.Set(ctx, key, data, 5*time.Minute).Err()
if err != nil {
// 此处通常记录日志,而不是直接返回错误,因为数据库查询已经成功
log.Printf("警告:数据写入缓存失败: %v", err)
}
return data, nil
}
// 模拟从数据库等数据源获取数据
func GetDataFromSource(key string) (string, error) {
// 这里模拟一个耗时的数据库查询
time.Sleep(100 * time.Millisecond)
result := mapstring{"id": key, "name": "示例数据"}
jsonData, _ := json.Marshal(result)
return string(jsonData), nil
}
func main() {
rdb := InitRedisClient()
defer rdb.Close() // 确保程序退出前关闭连接
data, err := GetData(rdb, "user:1001")
if err != nil {
log.Fatal(err)
}
fmt.Printf("获取到的数据: %s\n", data)
}
</pre></div>
<p class="maodian"><a name="_lab2_3_5"></a></p><h3>3.2 分布式锁</h3>
<p>在分布式系统中,当多个服务实例需要竞争同一个资源时,就需要分布式锁来保证互斥访问。Redis 因其单线程特性和原子操作,是实现分布式锁的常用方案。</p>
<p>一个健壮的分布式锁至少需要满足:</p>
<ol><li><strong>互斥性</strong>:在任意时刻,只有一个客户端能持有锁。</li><li><strong>避免死锁</strong>:即使客户端在持有锁期间崩溃,锁也能被自动释放。</li><li><strong>安全性</strong>:只能由锁的持有者来释放锁。</li></ol>
<p>以下是基于 Go 和 Redis 的实现示例,包含了安全的加锁和解锁逻辑。</p>
<div class="jb51code"><pre class="brush:go;">package main
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"time"
"github.com/go-redis/redis/v8"
)
// 用于原子释放锁的Lua脚本
// 先比较锁的值是否与当前客户端匹配,匹配才删除
var unlockScript = redis.NewScript(`
if redis.call("get", KEYS) == ARGV then
return redis.call("del", KEYS)
else
return 0
end
`)
type RedisLock struct {
client *redis.Client
ctx context.Context
}
func NewRedisLock(client *redis.Client) *RedisLock {
return &RedisLock{
client: client,
ctx: context.Background(),
}
}
// Lock 尝试获取分布式锁
func (rl *RedisLock) Lock(lockKey string, expireTime time.Duration) (bool, string, error) {
// 生成一个唯一的随机值作为锁的value,用于标识当前客户端
token, err := generateRandomToken()
if err != nil {
return false, "", err
}
// 使用SetNX命令,只有key不存在时才能设置成功,并设置过期时间
isSet, err := rl.client.SetNX(rl.ctx, lockKey, token, expireTime).Result()
if err != nil {
return false, "", err
}
return isSet, token, nil
}
// Unlock 安全地释放分布式锁
func (rl *RedisLock) Unlock(lockKey string, token string) error {
// 执行Lua脚本,确保判断锁归属和删除锁是原子操作
result, err := unlockScript.Run(rl.ctx, rl.client, []string{lockKey}, token).Int()
if err != nil {
return err
}
if result == 1 {
fmt.Println("锁释放成功")
} else {
fmt.Println("锁释放失败:可能不是锁的持有者或锁已过期")
}
return nil
}
// 生成随机token
func generateRandomToken() (string, error) {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(b), nil
}
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
defer rdb.Close()
lock := NewRedisLock(rdb)
lockKey := "my_distributed_lock"
expireTime := 10 * time.Second
// 尝试加锁
acquired, token, err := lock.Lock(lockKey, expireTime)
if err != nil {
log.Fatal(err)
}
if acquired {
fmt.Println("成功获取分布式锁")
// 模拟在锁保护下执行关键业务逻辑
fmt.Println("正在执行关键业务逻辑...")
time.Sleep(5 * time.Second)
fmt.Println("关键业务逻辑执行完毕")
// 业务完成,释放锁
err = lock.Unlock(lockKey, token)
if err != nil {
log.Fatal(err)
}
} else {
fmt.Println("获取分布式锁失败,可能有其他客户端正持有锁")
}
}
</pre></div>
<p><strong>关键要点与陷阱规避:</strong></p>
<ul><li><strong>原子性加锁</strong>:使用 <code>SET lock_name unique_value NX PX milliseconds</code>命令(或如示例中的 <code>SetNX</code>结合过期时间),确保设置值和过期时间是原子操作。</li><li><strong>安全释放锁</strong>:释放锁时,必须验证当前客户端是否是该锁的持有者。使用 Lua 脚本将判断和删除操作原子化,防止误删其他客户端持有的锁。</li><li><strong>自动续期(看门狗)</strong>:如果业务执行时间可能超过锁的过期时间,需要考虑实现一个自动续期机制(看门狗),在锁过期前自动延长持有时间。对于更复杂的场景,可以考虑使用现成的库,如 <code>go-redis-lock</code>。</li></ul>
<p class="maodian"><a name="_lab2_3_6"></a></p><h3>3.3 消息队列 (Pub/Sub 和基于 List)</h3>
<p>Redis 可以用于实现轻量级的消息队列,支持发布/订阅(Pub/Sub)模式和基于 List 的点对点模式。</p>
<p class="maodian"><a name="_label3_3_6_1"></a></p><h4>3.3.1 发布/订阅模式 (Pub/Sub)</h4>
<p>Pub/Sub 是一种广播模式,一个发布者向某个频道(channel)发送消息,所有订阅了该频道的订阅者都会收到消息。<strong>消息是即时的,没有持久化</strong>,如果订阅者离线,将收不到消息。</p>
<p><strong>发布者 (Publisher) 示例:</strong></p>
<div class="jb51code"><pre class="brush:go;">package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
ctx := context.Background()
for i := 1; i <= 5; i++ {
message := fmt.Sprintf("这是第 %d 条消息", i)
// 向 "news" 频道发布消息
err := rdb.Publish(ctx, "news", message).Err()
if err != nil {
panic(err)
}
fmt.Printf("发布消息: %s\n", message)
time.Sleep(1 * time.Second)
}
}
</pre></div>
<p><strong>订阅者 (Subscriber) 示例:</strong></p>
<div class="jb51code"><pre class="brush:go;">package main
import (
"context"
"fmt"
"log"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
ctx := context.Background()
// 订阅 "news" 频道
pubsub := rdb.Subscribe(ctx, "news")
defer pubsub.Close()
// 从频道接收消息
ch := pubsub.Channel()
for msg := range ch {
fmt.Printf("收到来自频道 %s 的消息: %s\n", msg.Channel, msg.Payload)
}
}
</pre></div>
<p class="maodian"><a name="_label3_3_6_2"></a></p><h4>3.3.2 基于 List 的队列</h4>
<p>使用 Redis 的 List 结构和 <code>LPUSH</code>/<code>BRPOP</code>命令可以实现一个更经典的点对点消息队列。消息可以被持久化,并且只能被一个消费者消费。</p>
<p><strong>生产者 (Producer) 示例:</strong></p>
<div class="jb51code"><pre class="brush:go;">package main
import (
"context"
"encoding/json"
"fmt"
"github.com/go-redis/redis/v8"
)
type Message struct {
ID string `json:"id"`
Content string `json:"content"`
}
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
ctx := context.Background()
queueName := "my_task_queue"
message := Message{ID: "1", Content: "需要处理的任务内容"}
jsonMsg, _ := json.Marshal(message)
// 将消息放入队列右侧 (尾部)
err := rdb.LPush(ctx, queueName, jsonMsg).Err()
if err != nil {
panic(err)
}
fmt.Println("消息已生产:", string(jsonMsg))
}
</pre></div>
<p><strong>消费者 (Consumer) 示例:</strong></p>
<div class="jb51code"><pre class="brush:go;">package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
ctx := context.Background()
queueName := "my_task_queue"
for {
// 从队列左侧 (头部) 阻塞地获取消息,超时时间设为0(无限等待)
result, err := rdb.BRPop(ctx, 0, queueName).Result() // result是队列名,result是消息体
if err != nil {
log.Fatal(err)
}
var msg Message
err = json.Unmarshal([]byte(result), &msg)
if err != nil {
log.Printf("消息解析失败: %v", err)
continue
}
fmt.Printf("开始处理消息: ID=%s, Content=%s\n", msg.ID, msg.Content)
// ... 这里处理业务逻辑 ...
fmt.Printf("消息 %s 处理完毕\n", msg.ID)
}
}
</pre></div>
<p><strong>关键要点与陷阱规避:</strong></p>
<ul><li><strong>模式选择</strong>:需要广播通知用 Pub/Sub;需要任务队列、保证消息至少被处理一次用基于 List 的队列。</li><li><strong>消息持久化</strong>:Pub/Sub 消息不持久化,List 消息会保存在 Redis 中。</li><li><strong>消费可靠性</strong>:List 队列中,消费者使用 <code>BRPOP</code>阻塞获取消息,但消息被取出后就在 Redis 中删除了。如果消费者处理失败,消息会丢失。对于要求可靠队列的场景,Redis 可能不是最佳选择,可以考虑更专业的消息中间件(如 RabbitMQ, Kafka)。</li></ul>
<p class="maodian"><a name="_lab2_3_7"></a></p><h3>3.4 实践总结与建议</h3>
<p>以上代码示例展示了 Redis 在 Go 语言中的典型应用。在实际项目中,还有一些通用建议:</p>
<ol><li><strong>连接管理</strong>:使用连接池,避免频繁创建和关闭连接。<code>go-redis</code>库默认使用了连接池。</li><li><strong>配置化</strong>:将 Redis 的地址、密码、DB 等配置信息放在配置文件(如 <code>app.ini</code>)中,提高灵活性。</li><li><strong>错误处理</strong>:对 Redis 操作进行完善的错误处理,区分是键不存在的正常情况还是网络错误等异常。</li><li><strong>框架选择</strong>:对于复杂的缓存需求,可以考虑使用封装好的框架,如 GoFrame 的 <code>gcache</code>模块,它提供了统一的缓存接口和更丰富的功能(如分布式锁、缓存适配器等)。</li></ol>
<p class="maodian"><a name="_label4"></a></p><h2>总结 </h2>
頁:
[1]