命令记不住

登陆

Redis 编程入门

Redis编程入门

0.前言

NoSQL

NoSQL(Not Only SQL),指的是非关系型的数据库。随着Web2.0的兴起,传统的关系数据库在应付Web2.0网站,特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展

而Go语⾔言作为21世纪的C语言,对NoQL的支持也是很好,目前流行的NoSQL主要有redis、mongoDB、 Cassandra和Membase等。这些数据库都有高性能、高并发读写等特点,目前已经广泛应用于各种应用中。

redis是一个key-value存储系统,类似还有Memcached。它支持存储的value类型相对更多,包括字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、带范围查询的排序集合(sorted sets)、位图(bitmaps)、hyperloglogs、带半径查询和流的地理空间索引等数据结构(geospatial indexes)。

  • Redis场景
redis也是一个应用类工具,一定想办法把它用起来。。。
ZSET:榜单、排行榜、定时任务
INCR点赞
LIST 管理后台发通知邮件/短信
去重
缓存 --> 把热点数据缓存起来,缩短响应时间
想获取更多的redis应用场景,就要多看书《Redis开发与运维》《redis实战》,多看网上大厂的技术分享
  • redis基础概念:
redis 常用的数据结构底层实现是什么? ziplist、跳跃表
缓存击穿、缓存雪崩 、缓存击穿。。。
  • redis优化
key的优化
key尽量量都要设置过期时间
一级key数量不要太多
bigkey尽量少
key分级
shop_userinfo_1000
shop_userinfo_2000
集群
持久化
AOF 和 RDB
GORM

1.Redis环境准备

安装Redis,本地docker启动redis server实例

docker run --name redisgo -p 6379:6379 -d redis:5.0.7

本地启动redis-cli连接上面的Redis server

docker run -it --network host --rm redis:5.0.7 redis-cli

安装go连接redis的客户端包
golang操作redis的客户端包有多个比如redigo、 go-redis。
github地址: https://github.com/gomodule/redigo
文档: https://godoc.org/github.com/gomodule/redigo/redis
我们可以直接通过go get来安装它

go get github.com/gomodule/redigo/redis
补充说明(优先使用go-redis)

某些旧项目里可能用到redigo比较多
推荐大家如果可以的话在新项目中就优先使用https://github.com/go-redis/redis

2.Go连接Redis

Conn接口是与Redis协作的主要接口,可以使用 Dial,DialWithTimeout或者NewConn 函数来创建连接,当任务完成时,应用程序必须调用Close函数来完成操作。

新建go文件(demo10_redis_open.go),示例代码:

package main

import (
	"fmt"
	"github.com/gomodule/redigo/redis"
)

func main() {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	fmt.Println("connect success ...")
	defer conn.Close()
}

3.命令操作

通过使用Conn接口中的do方法执行redis命令,常用redis命令: http://doc.redisfans.com/
go中发送与响应对应类型:
Do函数会必要时将参数转化为二进制字符串


| Go Type         | Conversion                          |
| --------------- | ----------------------------------- |
| []byte          | Sent as is                          |
| string          | Sent as is                          |
| int, int64      | strconv.FormatInt(v)                |
| float64         | strconv.FormatFloat(v, 'g', -1, 64) |
| bool            | true -> "1", false -> "0"           |
| nil             | ""                                  |
| all other types | fmt.Print(v)                        |

Redis 命令响应会用以下Go类型表示

| Redis type    | Go type                                   |
| ------------- | ----------------------------------------- |
| error         | redis.Error                               |
| integer       | int64                                     |
| simple string | string                                    |
| bulk string   | []byte or nil if value not present.       |
| array         | []interface{} or nil if value not present |

可以使用Go的类型断言或者reply辅助函数将返回的interface{}转换为对应类型。

3.1、key pattern 查询相应的key
(1)redis允许模糊查询key  有3个通配符  *、?、[]
(2)randomkey:返回随机key  
(3)type key:返回key存储的类型
(4)exists key:判断某个key是否存在
(5)del key:删除key
(6)rename key newkey:改名
(7)renamenx key newkey:如果newkey不存在则修改成功
(8)move key 1:将key移动到1数据库
(9)ttl key:查询key的生命周期(秒)
(10)expire key 整数值:设置key的生命周期以秒为单位
(11)pexpire key 整数值:设置key的生命周期以毫秒为单位
(12)pttl key:查询key 的生命周期(毫秒)
(13)perisist key:把指定key设置为永久有效
3.2、字符串类型的操作
(1)set key value [ex 秒数] [px 毫秒数] [nx/xx]  
    如果ex和px同时写,则以后面的有效期为准
    nx:如果key不存在则建立
    xx:如果key存在则修改其值
(2)get key:取值
(3)mset key1 value1 key2 value2 一次设置多个值
(4)mget key1 key2 :一次获取多个值
(5)setrange key offset value:把字符串的offset偏移字节改成value
                如果偏移量 > 字符串长度,该字符自动补0x00
(6)append key value :把value追加到key 的原值上
(7)getrange key start stop:获取字符串中[start, stop]范围的值
                对于字符串的下标,左数从0开始,右数从-1开始
                注意:当start>length,则返回空字符串
                   当stop>=length,则截取至字符串尾
                   如果start所处位置在stop右边,则返回空字符串
(8)getset key nrevalue:获取并返回旧值,在设置新值
(9)incr key:自增,返回新值,如果incr一个不是int的value则返回错误,incr一个不存在的key,则设置key为1
(10)incrby key 2:跳2自增
(11)incrbyfloat by 0.7: 自增浮点数 
(12)setbit key offset value:设置offset对应二进制上的值,返回该位上的旧值
               注意:如果offset过大,则会在中间填充0
                  offset最大到多少
                   2^32-1,即可推出最大的字符串为512M
(13)bitop operation destkey key1 [key2..]    对key1 key2做opecation并将结果保存在destkey上
                        opecation可以是AND OR NOT XOR
(14)strlen key:取指定key的value值的长度
(15)setex key time value:设置key对应的值value,并设置有效期为time秒
3.3、链表操作

Redis的list类型其实就是一个每个子元素都是string类型的双向链表,链表的最大长度是2^32。list既可以用做栈,也可以用做队列。
list的pop操作还有阻塞版本,主要是为了避免轮询

(1)lpush key value:把值插入到链表头部
(2)rpush key value:把值插入到链表尾部
(3)lpop key :返回并删除链表头部元素
(4)rpop key: 返回并删除链表尾部元素
(5)lrange key start stop:返回链表中[start, stop]中的元素
(6)lrem key count value:从链表中删除value值,删除count的绝对值个value后结束
              count > 0 从表头删除  count < 0 从表尾删除  count=0 全部删除
(7)ltrim key start stop:剪切key对应的链接,切[start, stop]一段并把改制重新赋给key
(8)lindex key index:返回index索引上的值
(9)llen key:计算链表的元素个数
(10)linsert key after|before search value:在key 链表中寻找search,并在search值之前|之后插入value
(11)rpoplpush source dest:把source 的末尾拿出,放到dest头部,并返回单元值
应用场景: task + bak 双链表完成安全队列
业务逻辑: rpoplpush task bak
     接收返回值并做业务处理
     如果成功则rpop bak清除任务,如果不成功,下次从bak表取任务
(12)brpop,blpop key timeout:等待弹出key的尾/头元素
     timeout为等待超时时间,如果timeout为0则一直等待下去
     应用场景:长轮询ajax,在线聊天时能用到
3.4、hashes类型及操作

Redis hash

是一个string类型的field和value的映射表,它的添加、删除操作都是O(1)(平均)。hash特别适用于存储对象,将一个对象存储在hash类型中会占用更少的内存,并且可以方便的存取整个对象。

配置: hash_max_zipmap_entries 64 #配置字段最多64个
    hash_max_zipmap_value 512 #配置value最大为512字节
(1)hset myhash field value:设置myhash的field为value
(2)hsetnx myhash field value:不存在的情况下设置myhash的field为value
(3)hmset myhash field1 value1 field2 value2:同时设置多个field
(4)hget myhash field:获取指定的hash field
(5)hmget myhash field1 field2:一次获取多个field
(6)hincrby myhash field 5:指定的hash field加上给定的值
(7)hexists myhash field:测试指定的field是否存在
(8)hlen myhash:返回hash的field数量
(9)hdel myhash field:删除指定的field
(10)hkeys myhash:返回hash所有的field
(11)hvals myhash:返回hash所有的value
(12)hgetall myhash:获取某个hash中全部的field及value 
3.5、集合结构操作

特点:无序性、确定性、唯一性

(1)sadd key value1 value2:往集合里面添加元素
(2)smembers key:获取集合所有的元素
(3)srem key value:删除集合某个元素
(4)spop key:返回并删除集合中1个随机元素(可以坐抽奖,不会重复抽到某人)   
(5)srandmember key:随机取一个元素
(6)sismember key value:判断集合是否有某个值
(7)scard key:返回集合元素的个数
(8)smove source dest value:把source的value移动到dest集合中
(9)sinter key1 key2 key3:求key1 key2 key3的交集
(10)sunion key1 key2:求key1 key2 的并集
(11)sdiff key1 key2:求key1 key2的差集
(12)sinterstore res key1 key2:求key1 key2的交集并存在res里 
3.6、有序集合

概念:它是在set的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,每次指定后,zset会自动按新的值调整顺序。可以理解为有两列的mysql表,一列存储value,一列存储顺序,操作中key理解为zset的名字。
和set一样sorted,sets也是string类型元素的集合,不同的是每个元素都会关联一个double型的score。sorted set的实现是skip list和hash table的混合体。
当元素被添加到集合中时,一个元素到score的映射被添加到hash table中,所以给定一个元素获取score的开销是O(1)。另一个score到元素的映射被添加的skip list,并按照score排序,所以就可以有序地获取集合中的元素。添加、删除操作开销都是O(logN)和skip list的开销一致,redis的skip list 实现是双向链表,这样就可以逆序从尾部去元素。sorted set最经常使用方式应该就是作为索引来使用,我们可以把要排序的字段作为score存储,对象的ID当元素存储。

(1)zadd key score1 value1:添加元素
(2)zrange key start stop [withscore]:把集合排序后,返回名次[start,stop]的元素  默认是升续排列  withscores 是把score也打印出来
(3)zrank key member:查询member的排名(升序0名开始)
(4)zrangebyscore key min max [withscores] limit offset N:集合(升序)排序后取score在[min, max]内的元素,并跳过offset个,取出N个
(5)zrevrank key member:查询member排名(降序 0名开始)
(6)zremrangebyscore key min max:按照score来删除元素,删除score在[min, max]之间
(7)zrem key value1 value2:删除集合中的元素
(8)zremrangebyrank key start end:按排名删除元素,删除名次在[start, end]之间的
(9)zcard key:返回集合元素的个数
(10)zcount key min max:返回[min, max]区间内元素数量
(11)zinterstore dest numkeys key1[key2..] [WEIGHTS weight1 [weight2...]] [AGGREGATE SUM|MIN|MAX]
求key1,key2的交集,key1,key2的权值分别是weight1,weight2
聚合方法用 sum|min|max
聚合结果 保存子dest集合内
注意:weights,aggregate如何理解?
    答:如果有交集,交集元素又有score,score怎么处理?aggregate num->score相加,min最小score,max最大score,另外可以通过weights设置不同的key的权重,交集时  score*weight
3.7、服务器相关命令
(1)ping:测定连接是否存活
(2)echo:在命令行打印一些内容
(3)select:选择数据库
(4)quit:退出连接
(5)dbsize:返回当前数据库中key的数目
(6)info:获取服务器的信息和统计
(7)monitor:实时转储收到的请求
(8)config get 配置项:获取服务器配置的信息
   config set 配置项  值:设置配置项信息
(9)flushdb:删除当前选择数据库中所有的key
(10)flushall:删除所有数据库中的所有的key
(11)time:显示服务器时间,时间戳(秒),微秒数
(12)bgrewriteaof:后台保存rdb快照
(13)bgsave:后台保存rdb快照
(14)save:保存rdb快照
(15)lastsave:上次保存时间
(16)shutdown [save/nosave]
注意:如果不小心运行了flushall,立即shutdown nosave,关闭服务器,然后手工编辑aof文件,去掉文件中的flushall相关行,然后开启服务器,就可以倒回原来是数据。如果flushall之后,系统恰好bgwriteaof了,那么aof就清空了,数据丢失。
(17)showlog:显示慢查询
问:多慢才叫慢?
答:由slowlog-log-slower-than 10000,来指定(单位为微秒)
问:服务器存储多少条慢查询记录
答:由slowlog-max-len 128,来做限制  

4.get/set操作

示例代码:

package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	fmt.Println("Redis连接成功")

	defer conn.Close()
	_, err = conn.Do("SET", "name", "opsxdev")
	if err != nil {
		fmt.Println("redis set error:", err)
	}
	name, err := redis.String(conn.Do("GET", "name"))
	if err != nil {
		fmt.Println("redis get error:", err)
	} else {
		fmt.Printf("Got name: %s \n", name)
	}
}

结果:

127.0.0.1:6379> get name
"opsxdev"

5.设置key过期时间

关键代码:

_, err = conn.Do("expire", "name", 5) // 5秒过期
if err != nil {
	fmt.Println("set expire error: ", err)
	return
}

main.go

package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	//连接redis
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	fmt.Println("Redis连接成功")

	defer conn.Close()
	//SET Redis
	_, err = conn.Do("SET", "name", "opsxdev")
	if err != nil {
		fmt.Println("redis set error:", err)
	}
	name, err := redis.String(conn.Do("GET", "name"))
	if err != nil {
		fmt.Println("redis get error:", err)
	} else {
		fmt.Printf("Got name: %s \n", name)
	}
	//设置过期时间
	_, err = conn.Do("expire", "name", 5)
	if err != nil {
		fmt.Println("set expire error: ", err)
		return
	}
}

g

g

6.批量mget/mset

关键代码:

_, err = conn.Do("MSET", "name", "lianshi", "age", 18)
if err != nil {
	fmt.Println("redis mset error:", err)
}
res, err := redis.Strings(conn.Do("MGET", "name", "age"))
if err != nil {
	fmt.Println("redis get error:", err)
} else {
	res_type := reflect.TypeOf(res)
	fmt.Printf("res type : %s \n", res_type)
	fmt.Printf("MGET name: %s \n", res)
	fmt.Println(len(res))
}

main.go

package main

import (
	"fmt"
	"reflect"

	"github.com/gomodule/redigo/redis"
)

func main() {
	//连接redis
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	fmt.Println("Redis连接成功")

	defer conn.Close()
	//批量
	_, err = conn.Do("MSET", "name", "opsxdev", "age", "27")
	if err != nil {
		fmt.Println("redis mset error:", err)
	}
	res, err := redis.Strings(conn.Do("MGET", "name", "age"))
	if err != nil {
		fmt.Println("redis get error:", err)
	} else {
		//类型断言,age虽然是27,实际是string
		res_type := reflect.TypeOf(res)
		fmt.Printf("res type : %s \n", res_type)
		fmt.Printf("MGET name: %s \n", res)
		fmt.Println(len(res))
	}
}

7.列表操作

  • LPUSH 和 LRANGE
res, err := redis.String(conn.Do("LPOP", "list1"))          //获取栈顶元素
res, err := redis.String(conn.Do("LINDEX", "list1", 3))     //获取指定位置的元素
res, err := redis.Strings(conn.Do("LRANGE", "list1", 0, 3)) //获取指定下标范围的元素

main.go

package main

import (
	"fmt"
	"reflect"

	"github.com/gomodule/redigo/redis"
)

func main() {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	fmt.Println("connect success ...")
	defer conn.Close()
	_, err = conn.Do("LPUSH", "list_opsxdev", "lis1", "lis2", "list3", "list4", "list5")
	if err != nil {
		fmt.Println("redis mset error:", err)
	}
	//res, err := redis.String(conn.Do("LPOP", "list_opsxdev"))
	//res, err := redis.String(conn.Do("LINDEX", "list_opsxdev", "0"))
	res, err := redis.Strings(conn.Do("LRANGE", "list1", 0, 3)) //获取指定下标范围的元素
	if err != nil {
		fmt.Println("redis POP error:", err)
	} else {
		res_type := reflect.TypeOf(res)
		fmt.Printf("res type : %s \n", res_type)
		fmt.Printf("res : %s \n", res)
	}
}

8.hash操作

  • HSET
  • HGET

hash类型是一个string类型的field和value的映射表,每个 hash 可以存储 232 - 1 键值对(40多亿)

主要代码:

_, err = conn.Do("HSET", "user", "name", "lianshi", "age", 18)
res, err := redis.Int64(conn.Do("HGET", "user", "age"))

hash:

// hash
_, err = conn.Do("HSET", "user", "name", "lianshi", "age", 18)
if err != nil {
	fmt.Println("redis mset error:", err)
}

// res 在上面已经被声明为 []string 类型,这里不能用它来接收 Int64
res2, err := redis.Int64(conn.Do("HGET", "user", "age"))
if err != nil {
	fmt.Println("redis HGET error:", err)
} else {
	resType := reflect.TypeOf(res2)
	fmt.Printf("res type : %s \n", resType)
	fmt.Printf("res  : %d \n", res2)
}
res3, err := redis.StringMap(conn.Do("HGETALL", "user"))
if err != nil {
	fmt.Println("redis HGETALL error:", err)
} else {
	resType := reflect.TypeOf(res3)
	fmt.Printf("res type : %s \n", resType)
	fmt.Printf("res  : %#v\n", res3)
}

main.go

package main

import (
	"fmt"
	"reflect"

	"github.com/gomodule/redigo/redis"
)

func main() {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	fmt.Println("connect success ...")
	defer conn.Close()
	_, err = conn.Do("HSET", "user", "name", "hopsxdev", "age", 18)
	if err != nil {
		fmt.Println("redis mset error:", err)
	}
	res, err := redis.Int64(conn.Do("HGET", "user", "age"))
	if err != nil {
		fmt.Println("redis HGET error:", err)
	} else {
		res_type := reflect.TypeOf(res)
		fmt.Printf("res type : %s \n", res_type)
		fmt.Printf("res : %d \n", res)
	}
	//结果:
	// connect success ...
	// res type : int64
	//res : 18
}

9.Pipeline管道

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

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

新建go文件(demo15_redis_pipelining.go),示例代码:

package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	fmt.Println("connect success ...")
	//把命令写入缓冲区
	conn.Send("HSET", "users", "name", "Popsxdev", "age", "2020")
	conn.Send("HSET", "users", "sex", "man", "female", "996")
	conn.Send("HSET", "user", "still")
	//把缓冲区的命令打包发给server端
	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:%v\n", res3)
	//会阻塞
	res4, err := conn.Receive()
	fmt.Printf("Receive res3:%v\n", res4)

}

//运行结果
C:\Users\vSphere\go\src\proetcd\lesson20_code\wu01>go run main.go
connect success ...
Receive res1:0
Receive res2:0
Receive res3:<nil>
阻塞

10.发布/订阅

redis本身具有发布订阅的功能,其发布订阅功能通过命令 SUBSCRIBE(订阅)/ PUBLISH(发布) 实现,并且发布订阅模式可以是多对多模式还可支持正则表达式,发布者可以向一个或多个频道发送消息,订阅者可订阅一个或者多个频道接受消息。

操作示例,示例中将使用两个goroutine分别担任发布者和订阅者角色进行演示:

新建go文件(demo16_redis_pub_sub.go):

package main

import (
	"fmt"
	"time"

	"github.com/gomodule/redigo/redis"
)

func Subs() { //订阅者
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	defer conn.Close()
	psc := redis.PubSubConn{conn}
	psc.Subscribe("channel1") //订阅channel1频道
	for {
		switch v := psc.Receive().(type) {
		case redis.Message: //如果是消息打印消息
			fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
		case redis.Subscription: //如果是订阅,打印订阅信息
			fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
		case error:
			fmt.Println(v)
			return
		}
	}
}
func Push(message string) { //发布者
	conn, _ := redis.Dial("tcp", "127.0.0.1:6379")
	_, err1 := conn.Do("PUBLISH", "channel1", message)
	if err1 != nil {
		fmt.Println("pub err: ", err1)
		return
	}
}
func main() {
	go Subs()
	time.Sleep(time.Second * 1)
	go Push("this is opsxdev.com")
	time.Sleep(time.Second * 5)
}

// channel1: subscribe 1
// channel1: message: this is lianshi

11.事务操作

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

  • MULTI :开启事务
  • EXEC :执行事务
  • DISCARD :取消事务
  • WATCH :监视事务中的键变化,一旦有改变则取消事务。
MULTI操作

新建go文件(demo17_redis_transaction.go),示例代码

package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {

	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	fmt.Println("connect success ...")
	defer conn.Close()
	//MULTI :开启事务
	conn.Send("MULTI")
	conn.Send("INCR", "foo") //累加1
	conn.Send("INCR", "bar")
	//执行事务
	r, err := conn.Do("EXEC")
	fmt.Println(r)
}
watch操作

WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。

package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		fmt.Println("connect redis error :", err)
		return
	}
	fmt.Println("connect success ...")
	defer conn.Close()
	conn.Do("SET", "xo", 10)
	conn.Do("WATCH", "xo")
	v, _ := redis.Int64(conn.Do("GET", "xo"))
	v = v + 1 // 这里可以基于值做一些判断逻辑
	conn.Send("MULTI")
	conn.Send("SET", "xo", v)
	r, err := conn.Do("EXEC")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(r)
}

在获取 xo 的值之前先通过 WATCH 命令监控了该键,此后又将 set 命令包围在事务中,这样就可以有效的保证每个连接在执行EXEC之前,如果当前连接获取的 xo 的值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。这样调用者在判断返回值后就可以获悉val是否被重新设置成功。

注意事项

  • 由于WATCH命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不不修改这⼀一键值,所以在一般的情况下我们需要在EXEC执行失败后重新执行整个函数。
  • 执行EXEC命令后会取消对所有键的监控,如果不想执行事务中的命令也可以使用 UNWATCH 命令来取消监控。

12.连接池的使用

redis连接池是通过pool结构体实现,以下是源码定义,相关参数说明已经备注

type Pool struct {
	// Dial is an application supplied function for creating and configuring a
	// connection.
	//
	// The connection returned from Dial must not be in a special state
	// (subscribed to pubsub channel, transaction started, ...).
	Dial func() (Conn, error) //连接方法
	// TestOnBorrow is an optional application supplied function for checking
	// the health of an idle connection before the connection is used again by
	// the application. Argument t is the time that the connection was returned
	// to the pool. If the function returns an error, then the connection is
	// closed.
	TestOnBorrow func(c Conn, t time.Time) error
	// Maximum number of idle connections in the pool.
	MaxIdle int //最大的空闲连接数,即使没有redis连接时依然可以保持N个空闲的连接,而不被清除,随时处于待命状态
	// Maximum number of connections allocated by the pool at a given time.
	// When zero, there is no limit on the number of connections in the pool.
	MaxActive int //最大的激活连接数,同时最多有N个连接
	// Close connections after remaining idle for this duration. If the value
	// is zero, then idle connections are not closed. Applications should set
	// the timeout to a value less than the server's timeout.
	IdleTimeout time.Duration //空闲连接等待时间,超过此时间后,空闲连接将被关闭
	// If Wait is true and the pool is at the MaxActive limit, then Get() waits
	// for a connection to be returned to the pool before returning.
	Wait bool //当配置项为true并且MaxActive参数有限制时候,使用Get方法等待一个连接返回给连接池
	// Close connections older than this duration. If the value is zero, then
	// the pool does not close connections based on age.
	MaxConnLifetime time.Duration
	// contains filtered or unexported fields
}

新建go文件(demo18_redis_pool.go),示例代码:

package main

import (
	"fmt"
	"time"

	"github.com/gomodule/redigo/redis"
)

var Pool redis.Pool

func initRedis(password string, db int) { // init 用于初始化一些参数,先于main执行
	Pool = redis.Pool{
		MaxIdle:     16,
		MaxActive:   32,
		IdleTimeout: 120,
		Dial: func() (redis.Conn, error) {
			c, err := redis.Dial("tcp", "127.0.0.1:6379",
				redis.DialConnectTimeout(30*time.Millisecond),
				redis.DialReadTimeout(5*time.Millisecond),
				redis.DialWriteTimeout(5*time.Millisecond),
			)
			if err != nil {
				fmt.Println(err)
				return nil, err
			}
			// auth认证
			if _, err := c.Do("AUTH", password); err != nil {
				c.Close()
				return nil, err
			}
			// 使用select指令选择数据库
			if _, err := c.Do("SELECT", db); err != nil {
				c.Close()
				return nil, err
			}
			return c, nil
		},
		//测试连接可用性的方法
		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			if time.Since(t) < time.Minute {
				return nil
			}
			_, err := c.Do("PING")
			return err
		},
	}
}

func someFunc() {
	conn := Pool.Get() // 从连接池里拿出一个连接
	defer conn.Close()
	res, err := conn.Do("HSET", "user", "name", "opsxdev")
	fmt.Println(res, err)
	res1, err := redis.String(conn.Do("HGET", "user", "name"))
	fmt.Printf("res:%s,error:%v\n", res1, err)
	//...
}
func main() {
	initRedis("", 0)
	someFunc()
}

//0 <nil>
//res:opsxdev,error:<nil>

密码认证和选择指定数据库

pool := &redis.Pool{
	// 其他配置同上,这里就省略了...
	Dial: func() (redis.Conn, error) {
		c, err := redis.Dial("tcp", server)
		if err != nil {
			return nil, err
		}
		// auth认证
		if _, err := c.Do("AUTH", password); err != nil {
			c.Close()
			return nil, err
		}
		// 使⽤用select指令选择数据库
		if _, err := c.Do("SELECT", db); err != nil {
			c.Close()
			return nil, err
		}
		return c, nil
	},
}

连接检测

在将连接返回给应用程序之前,使用 TestOnBorrow 函数检查空闲连接的健康状况。
下面的示例是对空闲超过一分钟的连接执行 ping 命令检测连接是否可用:

13.redis哨兵和集群

哨兵

// See http://redis.io/topics/sentinel for instructions how to
// setup Redis Sentinel.
rdb := redis.NewFailoverClient(&redis.FailoverOptions{
	MasterName: "master",
	SentinelAddrs: []string{"12.11.10.1:26379", "12.11.10.2:26379",
		"12.11.10.3:26379"},
})
rdb.Ping()

集群

codis redis-cluster
16384个插槽(slot) //为什么Redis集群有16384个槽https://zhuanlan.zhihu.com/p/80335611

// See http://redis.io/topics/cluster-tutorial for instructions
// how to setup Redis Cluster.
rdb := redis.NewClusterClient(&redis.ClusterOptions{
	Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
})
rdb.Ping()
推荐阅读