Golang Redis常用操作&复杂数据类型的缓存(redigo)

3,725 阅读4分钟

一、Redis简介

1. Redis是什么?

Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源、包含多种数据类型字符串类型(string),散列类型(hash),列表类型(list),集合类型(set),有序集合类型(zset)、支持网络、基于内存、可选持久性的键值对存储数据库。

2. 为什么要用Redis?

1. 解决应用服务器的cpu和内存压力
2. 减少io的读操作,减轻io的压力
3. 关系型数据库的扩展性不强,难以改变表结构。

通俗点的意思,就是因为redis直接作用于缓存,比关系型的数据库快得多,大多数时候是配合关系型数据库使用,避免访问频率高的数据反复作用于数据库,导致数据库的负载过高,减少了I/O操作

3.使用场景

  1. 热点数据缓存(本章介绍)
  2. 计数器
  3. 排行榜
  4. 分布式锁
  5. ......

4. Redis作缓存怎么用呢?

访问数据库之前,先去Redis查找有无结果,有就直接返回结果,否则再去数据库中查找,查找到之后在redis做缓存并返回结果。

二、 Golang Redis常用操作

推荐一个网址 redigo API文档

1. 第三方库

go get -u github.com/gomodule/redigo/redis

2. 连接redis

采用的是连接池的方式

package main

import (
   "fmt"
   _ "github.com/go-sql-driver/mysql"
   "github.com/gomodule/redigo/redis"
   "time"
)

var rds redis.Conn

func RedisPollInit() *redis.Pool {
   return &redis.Pool{
      MaxIdle:     5, //最大空闲数
      MaxActive:   0, //最大连接数,0不设上
      Wait:        true,
      IdleTimeout: time.Duration(1) * time.Second, //空闲等待时间
      Dial: func() (redis.Conn, error) {
         c, err := redis.Dial("tcp", "127.0.0.1:6379") //redis IP地址
         if err != nil {
            fmt.Println(err)
            return nil, err
         }
         redis.DialDatabase(0)
         return c, err
      },
   }
}

func RedisInit() {
   rds = RedisPollInit().Get()
}

func RedisClose() {
   _ = rds.Close()
}

3. Get&Set

func main(){
   RedisInit()
   defer RedisClose()

   var err error
   _, err = rds.Do("set", "name", "abc") //redis set命令
   if err != nil {
      fmt.Println(err)
      return
   }

   res, err := rds.Do("Get", "name") //redis get命令
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println("res:", res)

   res1, err := redis.String(rds.Do("Get", "name")) //redis get命令
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println("res1:",res1)

   res2, err := redis.Int(rds.Do("Get", "name")) //redis get命令
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println("res2:",res2)
}
得到结果->
res: [97 98 99]
res1: abc
strconv.ParseInt: parsing "abc": invalid syntax
解析:
  • rds.Do() 执行命令函数,返回的是空接口任意类型
Do(commandName string, args ...interface{}) (reply interface{}, err error)
  • redis.String() 把GET返回的空接口解析成string类型
func String(reply interface{}, err error) (string, error)
  • redis.Int() 把GET返回的空接口解析成int类型,因为"abc"无法转换成int类型,所以报错
func Int(reply interface{}, err error) (int, error)

4.过期设置

func main(){
   RedisInit()
   defer RedisClose()

   var err error
   _, err = rds.Do("set", "name", "abc", "EX", 5) //redis set命令
   if err != nil {
      fmt.Println(err)
      return
   }

   res, err := redis.String(rds.Do("Get", "name")) //redis get命令
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println("res:", res)

   time.Sleep(time.Second * 6)

   res, err = redis.String(rds.Do("Get", "name")) //redis get命令
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println("res:", res)
}
得到结果->
res: abc
redigo: nil returned
解析:
  • rds.Do("set", "name", "abc", "EX", 5) 执行命令函数,返回的是空接口任意类型,加入了EX过期命令和过期时间5s
Do(commandName string, args ...interface{}) (reply interface{}, err error)

5.分布式锁

func main(){
   RedisInit()
   defer RedisClose()

   var err error
   _, err = rds.Do("set", "name1", "abc", "NX", "EX", 5) //redis set命令
   if err != nil {
      fmt.Println(err)
      return
   }

   _, err = rds.Do("set", "name1", "叶叶子", "NX", "EX", 5) //redis set命令
   if err != nil {
      fmt.Println(err)
      return
   }

   res, err := redis.String(rds.Do("Get", "name1")) //redis get命令
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println("res:", res)

   time.Sleep(time.Second * 6)

   _, err = rds.Do("set", "name1", "叶叶子", "NX", "EX", 5) //redis set命令
   if err != nil {
      fmt.Println(err)
      return
   }

   res, err = redis.String(rds.Do("Get", "name1")) //redis get命令
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println("res:", res)
}
得到结果->
res: abc
res: 叶叶子

解析:
  • rds.Do("set", "name1", "abc", "NX", "EX", 5) 执行命令函数,NX命令:在指定的 key 不存在时,为 key 设置指定的值,否则跳过,可以看到过期之后才能继续设置值。
Do(commandName string, args ...interface{}) (reply interface{}, err error)

6.结构体切片map等等复杂类型缓存处理

type User struct {
   Name string
   Age  int
}

func main() {
   RedisInit()
   defer RedisClose()
   var err error

   var user = make([]User, 0)
   user = []User{
      {Name: "叶叶子", Age: 18},
      {Name: "abc", Age: 1},
   }

   data, err := json.Marshal(user)
   _, err = rds.Do("set", "user", data , "EX", 5) //redis set命令
   if err != nil {
      fmt.Println(err)
      return
   }

   var res = make([]User, 0)
   r, err := redis.Bytes(rds.Do("get", "user"))
   if len(r) > 0 {
      err = json.Unmarshal(r, &res)
      if err != nil {
         fmt.Println(err)
         return
      }
      fmt.Println("使用redis", res)
   }
}
得到结果->
使用redis [{叶叶子 18} {abc 1}]
解析:
  • 使用"encoding/json"json.Marshal,把数据转序列化成json数据存入redis,取出key值的时候使用redis.Bytes()解析之后再用json.Unmarshal反序列化得到结果。

三、Redis风险&解决办法

参考文档redis缓存雪崩、穿透、击穿概念及解决办法

1、缓存雪崩

  • 场景

假设缓存上限最多1秒应答100个请求,但是高峰期每秒200个请求,缓存出现意外,导致请求全部作用到数据库,导致数据库挂掉。

  • 解决办法
  1. redis使用高可用的部署,主从、哨兵、集群的模式;
  2. 后端限流降级处理;
  3. redis持久化,可快速恢复。

2、缓存穿透

  • 场景

假设缓存上限最多1秒应答100个请求,恶意有人1秒发出100个缓存和数据库都查不到的请求,导致每次都回去查数据库。

  • 解决办法

从数据库中只要没查到,就写一个空值到缓存里。

2、缓存击穿

  • 场景

热点key时效是,大量请求直接作用到数据库。

  • 解决办法

热点数据不过期。