Skip to main content
 首页 » 数据库

redis在go中使用小结

2022年07月19日158cloudgamer

最近做了个关于redis的项目,那么就整理下遇到和未遇到的问题

1、redis的简介安装

2、redis的数据结构

3、Redis基本使用

4、Redis的并发

5、Redis的落地

一、redis的简介安装

 

一、Redis 是什么

  Redis 是一款依据BSD开源协议发行的高性能Key-Value存储系统(cache and store)。它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) , 有序集合(sorted sets)和位图(bitmaps)等类型。官方网站是 http://redis.io/

  Redis 和其它 NO SQL 的比较本文不做过多阐述。我觉得 Redis 最好的地方就是提供数据持久化功能(定时把内存中的数据写入文件),从而不至于一旦宕机将造成数据丢失。而且相较于 Memcached ,它提供的值类型选择更为宽泛。

二、Redis 下载安装  

  打开 Redis 官网,我们发现 Redis 官方并不支持 Windows 平台,但 Microsoft Open Tech Group 却改变了这一情况

  点击 Learn more

  点击 Download ZIP, 下载完后解压,我们发现其并没有提供现成的执行安装文件,这就需要我们自行进行编译。定位到目录 Redis\redis2.8\msvs,打开文件 RedisServer.sln

 

  项目结构如下图

  由于笔者的机器为64位,在编译之前我们确认一下编译 Platform, 同时我们可以看到对于此 project 将会编译产生 redis-server.exe 文件

   其它项目类似

   编译成功之后,我们到其 Debug 目录下找到编译产生的文件

  为了便于处理,我们新建目录 Redis,并把这些文件拷贝过去

  其中的 redis.conf 来自如下目录

  至此,我们已经得到所有需要的文件了,下面就可以使用了,打开 CMD, 定位到目录 D:\Developer\Redis\Redis,然后执行如下命令

redis-server.exe redis.conf

  执行成功的截图(可以看到端口为6379, 进程标识符 PID 为7696)

  执行过程中还会读取配置,由于截图太长,故这里放出文字

 

  Server 端好了,现在我们开一个 Client 端来测试一下,新打开 CMD (之前打开的 CMD - Server 端不能关闭)

redis-cli.exe -h 10.7.15.172 -p 6379

   其中 10.7.15.172 为本机 IP

set hello helloworld

  设置一个值

get hello

  读取这个值

  大概15分钟之后我们发现 Server 端也有变化

  原来15分钟自动把内存中的数据写入 RDF 文件以防丢失。

  至于为什么是15分钟,我们可以看到配置文件是这样设置的(1个更改/900秒,10更改/300秒,10000更改/60秒),即更改的越多,数据写入文件的时间间隔越短,这样设计蛮合理的。

三、Redis Desktop Manager

   虽然通过上面的 CMD 我们也能看到 Redis 在内存中的数据,但方式太不友好了,这里介绍一个工具 Redis Desktop Manager

  下载完成后安装,之后连接至 Server 即可

 

   点击查看数据

四、Install Redis as Windows Service

   前面我们通过 CMD 方式安装了Redis, 但是非常不方便,因为我们要一直保持窗口打开,而且如果机器重启的话也需要重新打开。Redis 也可以以 Windows Service 的方式进行部署。在部署之前我们需要把配置文件找到

  然后拷贝到 Redis 目录

  安装服务

redis-server --service-install redis.windows.conf

  安装成功提示

  查看服务

  启动服务

redis-server --service-start

  停止服务

redis-server --service-stop

  卸载服务

redis-server --service-uninstall

  其实安装成 Windows 服务还有一种方式,就是从 Github 上直接下载安装文件,但是好像不是最新的版本

二、Redis中的数据类型

redis是键值对的数据库,有5中主要数据类型

字符串类型(string),散列类型(hash),列表类型(list),集合类型(set),有序集合类型(zset)

几个基本的命令:

KEYS * 获得当前数据库的所有键

EXISTS key [key ...]  判断键是否存在,返回个数,如果key有一样的也是叠加数

DEL key [key ...]       删除键,返回删除的个数

TYPE key                  获取减值的数据类型(string,hash,list,set,zset)

FLUSHALL                清空所有数据库

CONFIG [get、set]    redis配置

-inf 负无穷

+inf正无穷

一:字符串类型string

字符串类型是Redis的最基本类型,它可以存储任何形式的字符串。其它的四种类型都是字符串类型的不同形式。

最基本的命令:GET、SET         语法:GET key,SET key value   value如果有空格需要双引号以示区分

整数递增:INCR                      语法:INCR key    默认值为0,所以首先执行命令得到 1 ,不是整型提示错误

增加指定的整数:INCRBY          语法:INCRBY key increment

整数递减:DECR                     语法:DECR key   默认值为0,所以首先执行命令得到 -1,不是整型提示错误

减少指定的整数:DECRBY         语法:DECRBY key increment

增加指定浮点数:INCRBYFLOAT 语法:INCRBYFLOAT key increment  与INCR命令类似,只不过可以递增一个双精度浮点数

向尾部追加值:APPEND             语法:APPEND key value   redis客户端并不是输出追加后的字符串,而是输出字符串总长度

获取字符串长度:STRLEN          语法:STRLEN key  如果键不存在返回0,注意如果有中文时,一个中文长度是3,redis是使用UTF-8编码中文的

获取多个键值:MGET                语法:MGET key [key ...]  例如:MGET key1 key2 

设置多个键值:MSET                语法:MSET key value [key value ...]  例如:MSET key1 1 key2 "hello redis"

二进制指定位置值:GETBIT        语法:GETBIT key offset   例如:GETBIT key1 2 ,key1为hello 返回 1,返回的值只有0或1,

                   当key不存在或超出实际长度时为0

设置二进制位置值:SETBIT       语法:SETBIT key offset value ,返回该位置的旧值

二进制是1的个数:BITCOUNT    语法:BITCOUNT key [start end] ,start 、end为开始和结束字节

位运算:BITOP                       语法:BITOP operation destkey key [key ...]  ,operation支持AND、OR、XOR、NOT

偏移:BITPOS                        语法:BITPOS key bit [start] [end]

二:散列类型hash

设置单个:HSET                      语法:HSET key field value,不存在时返回1,存在时返回0,没有更新和插入之分

设置多个:HMSET                    语法:HMSET key field value [field value ...]

读取单个:HGET                      语法:HGET key field,不存在是返回nil

读取多个:HMGET                    语法:HMGET key field [field ...]

读取全部:HGETALL                 语法:HGETALL key,返回时字段和字段值的列表

判断字段是否存在:HEXISTS      语法:HEXISTS key field,存在返回1 ,不存在返回0

字段不存在时赋值:HSETNX       语法:HSETNX key field value,与hset命令不同,hsetnx是键不存在时设置值

增加数字:HINCRBY                 语法:HINCRBY key field increment ,返回增加后的数,不是整数时会提示错误

删除字段:HDEL                      语法:HDEL key field [field ...] ,返回被删除字段的个数

只获取字段名:HKEYS               语法:HKEYS key ,返回键的所有字段名

只获取字段值:HVALS              语法:HVALS key  ,返回键的所有字段值

字段数量:HLEN                      语法:HLEN key ,返回字段总数

三:列表类型(list)

内部使用双向链表实现,所以获取越接近两端的元素速度越快,但通过索引访问时会比较慢

添加左边元素:LPUSH               语法:LPUSH key value [value ...]  ,返回添加后的列表元素的总个数

添加右边元素:RPUSH              语法:RPUSH key value [value ...]  ,返回添加后的列表元素的总个数

移除左边第一个元素:LPOP        语法:LPOP key  ,返回被移除的元素值

移除右边第一个元素:RPOP        语法:RPOP key ,返回被移除的元素值 

列表元素个数:LLEN                语法:LLEN key, 不存在时返回0,redis是直接读取现成的值,并不是统计个数

获取列表片段:LRANGE           语法:LRANGE key start stop,如果start比stop靠后时返回空列表,0 -1 返回整个列表

                                                    正数时:start 开始索引值,stop结束索引值(索引从0开始)

                                                    负数时:例如 lrange num -2 -1,-2表示最右边第二个,-1表示最右边第一个,

删除指定值:LREM                  语法:LREM key count value,返回被删除的个数

                                                   count>0,从左边开始删除前count个值为value的元素

                                                   count<0,从右边开始删除前|count|个值为value的元素

                                                   count=0,删除所有值为value的元素

索引元素值:LINDEX               语法:LINDEX key index ,返回索引的元素值,-1表示从最右边的第一位

设置元素值:LSET                  语法:LSET key index value

保留列表片段:LTRIM              语法:LTRIM key start stop,start、top 参考lrange命令

一个列表转移另一个列表:RPOPLPUSH      语法:RPOPLPUSH source desctination ,从source列表转移到desctination列表,

                                                                 该命令分两步看,首先source列表RPOP右移除,再desctination列表LPUSH

四:集合类型(set)

集合类型值具有唯一性,常用操作是向集合添加、删除、判断某个值是否存在,集合内部是使用值为空的散列表实现的。

添加元素:SADD                    语法:SADD key member [member ...] ,向一个集合添加一个或多个元素,因为集合的唯一性,所以添加相同值时会被忽略。

                        返回成功添加元素的数量。

删除元素:SREM                    语法:SREM key member [member ...] 删除集合中一个或多个元素,返回成功删除的个数。

获取全部元素:SMEMBERS      语法:SMEMBERS key ,返回集合全部元素

值是否存在:SISMEMBER        语法:SISMEMBER key member ,如果存在返回1,不存在返回0

差运算:SDIFF                      语法:SDIFF key [key ...] ,例如:集合A和集合B,差集表示A-B,在A里有的元素B里没有,返回差集合;多个集合(A-B)-C

交运算:SINTER                语法:SINTER key [key ...],返回交集集合,每个集合都有的元素

并运算:SUNION        语法:SUNION key [key ...],返回并集集合,所有集合的元素

集合元素个数:SCARD           语法:SCARD key ,返回集合元素个数

集合运算后存储结果                语法:SDIFFSTROE destination key [key ...] ,差运算并存储到destination新集合中

                   SINTERSTROE destination key [key ...],交运算并存储到destination新集合中

                                                  SUNIONSTROE destination key [key ...],并运算并存储到destination新集合中

随机获取元素:SRANDMEMGER 语法:SRANDMEMBER key [count],根据count不同有不同结果,count大于元素总数时返回全部元素

                  count>0 ,返回集合中count不重复的元素

                  count<0,返回集合中count的绝对值个元素,但元素可能会重复

弹出元素:SPOP                     语法:SPOP key [count] ,因为集合是无序的,所以spop会随机弹出一个元素

五:有序集合类型

添加集合元素:ZADD              语法:ZADD key [NX|XX] [CH] [INCR] score member [score member ...],不存在添加,存在更新。

获取元素分数:ZSCORE          语法:ZSCORE key member ,返回元素成员的score 分数

元素小到大:ZRANGE             语法:ZRANGE key start top [WITHSCORES] ,参考LRANGE ,加上withscores 返回带元素,即元素,分数

                                                  当分数一样时,按元素排序

元素大到小:ZREVRANGE       语法:ZREVRANGE key start [WITHSCORES] ,与zrange区别在于zrevrange是从大到小排序

指定分数范围元素:ZRANGEBYSCORE   语法:ZRANGEBYSCORE key min max [WITHSCORE] [LIMIT offest count]

                返回从小到大的在min和max之间的元素,( 符号表示不包含,例如:80-100,(80 100,

                  withscore返回带分数

                  limit offest count 向左偏移offest个元素,并获取前count个元素

指定分数范围元素:ZREVRANGESCORE   语法:ZREVRANGEBYSCORE key max  min [WITHSCORE] [LIMIT offest count]

                与zrangebyscore类似,只不过该命令是从大到小排序的。

增加分数:ZINCRBY                语法:ZINCRBY key increment member ,注意是增加分数,返回增加后的分数;如果成员不存在,则添加一个为0的成员。

三、GO中Redis简使用

连接

import "github.com/garyburd/redigo/redis" 
 
func main() { 
    c, err := redis.Dial("tcp", "localhost:6379") 
    if err != nil { 
        fmt.Println("conn redis failed, err:", err) 
        return 
    } 
    defer c.Close() 
}

set & get

        _, err = c.Do("Set", "name", "nick") 
    if err != nil { 
        fmt.Println(err) 
        return 
    } 
 
    r, err := redis.String(c.Do("Get", "name")) 
    if err != nil { 
        fmt.Println(err) 
        return 
    } 
    fmt.Println(r)

mset & mget

批量设置

        _, err = c.Do("MSet", "name", "nick", "age", "18") 
    if err != nil { 
        fmt.Println("MSet error: ", err) 
        return 
    } 
 
    r2, err := redis.Strings(c.Do("MGet", "name", "age")) 
    if err != nil { 
        fmt.Println("MGet error: ", err) 
        return 
    } 
    fmt.Println(r2)

hset & hget

hash操作

    _, err = c.Do("HSet", "names", "nick", "suoning") 
    if err != nil { 
        fmt.Println("hset error: ", err) 
        return 
    } 
 
    r, err = redis.String(c.Do("HGet", "names", "nick")) 
    if err != nil { 
        fmt.Println("hget error: ", err) 
        return 
    } 
    fmt.Println(r)

expire

设置过期时间

    _, err = c.Do("expire", "names", 5) 
    if err != nil { 
        fmt.Println("expire error: ", err) 
        return 
    }

lpush & lpop & llen

队列

    // 队列 
    _, err = c.Do("lpush", "Queue", "nick", "dawn", 9) 
    if err != nil { 
        fmt.Println("lpush error: ", err) 
        return 
    } 
    for { 
        r, err = redis.String(c.Do("lpop", "Queue")) 
        if err != nil { 
            fmt.Println("lpop error: ", err) 
            break 
        } 
        fmt.Println(r) 
    } 
    r3, err := redis.Int(c.Do("llen", "Queue")) 
    if err != nil { 
        fmt.Println("llen error: ", err) 
        return 
    }

连接池

各参数的解释如下:

MaxIdle:最大的空闲连接数,表示即使没有redis连接时依然可以保持N个空闲的连接,而不被清除,随时处于待命状态。

MaxActive:最大的激活连接数,表示同时最多有N个连接

IdleTimeout:最大的空闲连接等待时间,超过此时间后,空闲连接将被关闭

    pool := &redis.Pool{ 
        MaxIdle:     16, 
        MaxActive:   1024, 
        IdleTimeout: 300, 
        Dial: func() (redis.Conn, error) { 
            return redis.Dial("tcp", "localhost:6379") 
        }, 
    }

连接池栗子

package main 
 
import ( 
    "fmt" 
 
    "github.com/garyburd/redigo/redis" 
) 
 
var pool *redis.Pool 
 
func init() { 
    pool = &redis.Pool{ 
        MaxIdle:     16, 
        MaxActive:   1024, 
        IdleTimeout: 300, 
        Dial: func() (redis.Conn, error) { 
            return redis.Dial("tcp", "localhost:6379") 
        }, 
    } 
} 
 
func main() { 
    c := pool.Get() 
    defer c.Close() 
 
    _, err := c.Do("Set", "name", "nick") 
    if err != nil { 
        fmt.Println(err) 
        return 
    } 
 
    r, err := redis.String(c.Do("Get", "name")) 
    if err != nil { 
        fmt.Println(err) 
        return 
    } 
    fmt.Println(r) 
}

管道操作

请求/响应服务可以实现持续处理新请求,客户端可以发送多个命令到服务器而无需等待响应,最后在一次读取多个响应。

使用Send(),Flush(),Receive()方法支持管道化操作

Send向连接的输出缓冲中写入命令。

Flush将连接的输出缓冲清空并写入服务器端。

Recevie按照FIFO顺序依次读取服务器的响应。

func main() { 
    c, err := redis.Dial("tcp", "localhost:6379") 
    if err != nil { 
        fmt.Println("conn redis failed, err:", err) 
        return 
    } 
    defer c.Close() 
 
    c.Send("SET", "name1", "sss1") 
    c.Send("SET", "name2", "sss2") 
 
    c.Flush() 
 
    v, err := c.Receive() 
    fmt.Printf("v:%v,err:%v\n", v, err) 
    v, err = c.Receive() 
    fmt.Printf("v:%v,err:%v\n", v, err) 
 
    v, err = c.Receive()    // 夯住,一直等待 
    fmt.Printf("v:%v,err:%v\n", v, err)

四、Redis的并发

在日常的开发中,有时我们会遇到这样的场景:多个人对同一个数据进行修改操作,导致并发问题发生。这个问题可以通过悲观锁来解决,但是悲观锁也是有限制的,在某些场景中是不适应的,因为和数据的耦合度太高了,可能会影响到其他业务的操作。而使用redis来解决这一问题是很好的选择。

原理介绍

redis的存储指令中有一个setnx方法,这个方法有一个特性,就是当键不存在的时候,会将这条数据插入,并且返回1,如果这个键已经存在了,那么就不会插入这条数据,并且返回0。

功能实现

明白了这个实现的原理之后,要实现这个功能就很简单了。

  1. 在事务开启的时候,我们就去redis中setnx一条数据,这条数据的键要和你当前操作的数据有关,这样就只会锁定一条数据,而不影响其他数据的业务,例如:做订单审核的时候,将订单号+业务简写作为键。
  2. 判断上面插入操作的返回值,如果返回1,就继续执行,如果返回0,直接return.
  3. 在事务结束之后,将redis中的这条数据删除。直接使用del(String key)就可以了。

 go操作Redis不得不提就是Pipelining(管道)

管道操作可以理解为并发操作,并通过Send(),Flush(),Receive()三个方法实现。客户端可以使用send()方法一次性向服务器发送一个或多个命令,命令发送完毕时,使用flush()方法将缓冲区的命令输入一次性发送到服务器,客户端再使用Receive()方法依次按照先进先出的顺序读取所有命令操作结果。

Send(commandName string, args ...interface{}) error 
Flush() error 
Receive() (reply interface{}, err error)
  • Send:发送命令至缓冲区
  • Flush:清空缓冲区,将命令一次性发送至服务器
  • Recevie:依次读取服务器响应结果,当读取的命令未响应时,该操作会阻塞。

示例:

package main 
 
import ( 
"github.com/garyburd/redigo/redis" 
"fmt" 
) 
 
 
func main()  { 
    conn,err := redis.Dial("tcp","10.1.210.69:6379") 
    if err != nil { 
        fmt.Println("connect redis error :",err) 
        return 
    } 
    defer conn.Close() 
    conn.Send("HSET", "student","name", "wd","age","22") 
    conn.Send("HSET", "student","Score","100") 
    conn.Send("HGET", "student","age") 
    conn.Flush() 
 
    res1, err := conn.Receive() 
    fmt.Printf("Receive res1:%v \n", res1) 
    res2, err := conn.Receive() 
    fmt.Printf("Receive res2:%v\n",res2) 
    res3, err := conn.Receive() 
    fmt.Printf("Receive res3:%s\n",res3) 
} 
//Receive res1:0  
//Receive res2:0 
//Receive res3:22 

  

事务操作

MULTI, EXEC,DISCARD和WATCH是构成Redis事务的基础,当然我们使用go语言对redis进行事务操作的时候本质也是使用这些命令。

MULTI:开启事务

EXEC:执行事务

DISCARD:取消事务

WATCH:监视事务中的键变化,一旦有改变则取消事务。

示例:

package main 
 
import ( 
"github.com/garyburd/redigo/redis" 
"fmt" 
) 
 
 
func main()  { 
    conn,err := redis.Dial("tcp","10.1.210.69:6379") 
    if err != nil { 
        fmt.Println("connect redis error :",err) 
        return 
    } 
    defer conn.Close() 
    conn.Send("MULTI") 
    conn.Send("INCR", "foo") 
    conn.Send("INCR", "bar") 
    r, err := conn.Do("EXEC") 
    fmt.Println(r) 
} 
//[1, 1] 

四、Redis的落地

 

Redis 的落地策略其实就是持久化(Persistence),主要有以下2种策略:

  1. RDB: 定时快照方式(snapshot)
  2. AOF: 基于语句追加文件的方式

RDB

RDB 文件非常紧凑,它保存了 Redis 某个时间点上的数据集。RDB 恢复大数据集时速度要比 AOF 快。但是 RDB 不适合那些对时效性要求很高的业务,因为它只保存了快照,在进行恢复时会导致一些时间内的数据丢失。实际在进行备份时,Redis 主要依靠 rdbSave() 函数,然后有两个命令会调用这个函数 SAVE 和 BGSAVE,前者会同步调用,阻塞主进程导致会有短暂的 Redis-server 停止工作,后者会 fork 出子进程异步处理。

在调用 SAVE 或者 BGSAVE 时,只有发布和订阅功能的命令可以正常执行,因为这个模块和服务器的其他模块是隔离的。
下面的命令表示: “60 秒内有至少有 1000 个键被改动”时进行RDB文件备份。

redis-server> SAVE 60 1000

RDB 文件的结构

开头的REDIS表示这是一个 RDB 文件,然后紧跟着 redis 的版本号,SELECT-DB 和 KEY-VALUES-PAIRS 构成了对一个数据库中的所有数据记录,其中 KEY-VALUES-PAIRS具体结构如下,后面两个就不用说了。

其中对于不同的类型,RDB文件中有不同的 layout,具体就不写出来了。

AOF

AOF 可以通过设置的 fsync 策略配置,如果未设置 fsync ,AOF 的默认策略为每秒钟 fsync 一次,在这种配置下, fsync 会在后台线程执行,所以主线程不会受到打扰。但是像 AOF 这种策略会导致追加的文件非常大,而且在恢复大数据时非常缓慢,因为要把所有会导致写数据库的命令都重新执行一遍。AOF文件中实际存储的是 Redis 协议下的命令记录,因此非常易读。

当然 Redis 考虑到了 AOF 文件过大的问题,因此引入了 BGREWRITEAOF 命令进行重建 AOF 文件,保证可以减少大量无用的重复写操作。重建命令并不会去分析已有的 AOF 文件,而是将当前数据库的快照保存。

在 AOF 文件重写时,Redis 的具体逻辑如下:

  1. Redis 首先 fork 出一个子进程,子进程将新 AOF 文件的内容写入到临时文件。
  2. 对于所有新执行的写入命令,父进程一边将它们累积到一个缓存中,一边将这些改动追加到现有 AOF 文件的末尾: 这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
  3. 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将缓存中的所有数据追加到新 AOF 文件的末尾。
  4. 现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。

Redis 会维持一个默认的AOF重写策略,当当前的AOF文件比上次重写之后的文件大小增大了一倍时,就会自动在后台重写AOF。


本文参考链接:https://www.cnblogs.com/ricklz/p/9562722.html