Redis简介
NoSQL(Not Only SQL),指的是非关系型的数据库。随着Web2.0的兴起,传统的关系数据库在应付Web2.0网站,特别是超大规模和高并发的SNS类型的Web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。
redis是一个key-value存储系统,类似还有Memcached。它支持存储的value类型相对更多,包括字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、带范围查询的排序集合(sorted sets)、位图(bitmaps)、hyperloglogs、带半径查询和流的地理空间索引等数据结构(geospatial indexes)。
Redis操作
连接
Conn接口是与Redis协作的主要接口,可以使用Dial,DialWithTimeout或者NewConn
函数来创建连接,当任务完成时,应用程序必须调用Close函数来完成操作。
// 拨号连接
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()
get、set
- get
_, err = conn.Do("SET", "name", "lianshi")
if err != nil {
fmt.Println("redis set error:", err)
}
- set
// 转换成string
name, err := redis.String(conn.Do("GET", "name"))
if err != nil {
fmt.Println("redis get error:", err)
} else {
fmt.Printf("Get name: %s \n", name)
}
设置key的过期时间
_, err = conn.Do("expire", "name", 10) // 10秒过期
if err != nil {
fmt.Println("set expire error: ", err)
return
}
批量set、get
// 批量设置 或者 获取
_, 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))
}
链表操作
// 链表操作
_, err = conn.Do("LPUSH", "list1", "ele1", "ele2", "ele3", "ele4")
if err != nil {
fmt.Println("redis mset error:", err)
}
//res1, err := redis.String(conn.Do("LPOP", "list1"))//获取栈顶元素
//res1, err := redis.String(conn.Do("LINDEX", "list1", 3)) //获取指定位置的元素
res1, err := redis.Strings(conn.Do("LRANGE", "list1", 0, 3)) //获取指定下标范围的元素
if err != nil {
fmt.Println("redis POP error:", err)
} else {
res_type := reflect.TypeOf(res1)
fmt.Printf("res type : %s \n", res_type)
fmt.Printf("res : %s \n", res1)
}
哈希操作
// 哈希操作
_, err = conn.Do("HSET", "user", "name", "lianshi", "age", 18)
if err != nil {
fmt.Println("redis mset error:", err)
}
res2, err := redis.Int64(conn.Do("HGET", "user", "age"))
if err != nil {
fmt.Println("redis HGET error:", err)
} else {
res_type := reflect.TypeOf(res2)
fmt.Printf("res type : %s \n", res_type)
fmt.Printf("res : %d \n", res2)
}
管道
管道操作可以理解为并发操作,并通过Send(),Flush(),Receive()
三个方法实现。客户端可以使用send()方法一次性向服务器发送一个或多个命令,命令发送完毕时,使用flush()方法将缓冲区的命令输入一次性发送到服务器,客户端再使用Receive()
方法依次按照先进先出的顺序读取所有命令操作结果。
- Send:发送命令至缓冲区
- Flush:清空缓冲区,将命令一次性发送至服务器
- Recevie:依次读取服务器响应结果,当读取的命令未响应时,该操作会阻塞。
// Pipeline(管道) 对多个命令进行打包,装入缓冲区,然后一次发送,减少网络传输时间
conn.Send("HSET", "user", "name", "lianshi", "age", "30")
conn.Send("HSET", "user", "sex", "female")
conn.Send("HGET", "user", "age")
/// 一次性发送多个命令
conn.Flush()
// 调用一次conn.Receive(),即获取一次结果
res3, err := conn.Receive()
fmt.Printf("Receive res3:%v \n", res3)
res4, err := conn.Receive()
fmt.Printf("Receive res4:%v\n", res4)
res5, err := conn.Receive()
fmt.Printf("Receive res5:%s\n", res5)
发布、订阅
redis本身具有发布订阅的功能,其发布订阅功能通过命令SUBSCRIBE(订阅)/PUBLISH(发布)
实现,并且发布订阅模式可以是多对多模式还可支持正则表达式,发布者可以向一个或多个频道发送消息,订阅者可订阅一个或者多个频道接受消息。
示例中将使用两个goroutine分别担任发布者和订阅者角色进行演示:
package main
import (
"fmt"
"github.com/gomodule/redigo/redis"
"time"
)
// 订阅者
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()
go Push("this is me")
time.Sleep(time.Second * 5)
}
事务
// 事务
conn.Send("MULTI")
conn.Send("INCR", "foo")
conn.Send("INCR", "bar")
r, err := conn.Do("EXEC")
fmt.Println(r)
Watch
在获取xo
的值之前先通过WATCH
命令监控了该键,此后又将set
命令包围在事务中,这样就可以有效的保证每个连接在执行EXEC之前,如果当前连接获取的xo
的值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。这样调用者在判断返回值后就可以获悉val是否被重新设置成功。
// Watch监控
conn.Do("SET", "xo", 10)
conn.Do("WATCH", "xo")
v, _ := redis.Int64(conn.Do("GET", "xo"))
v = v + 1 // 这里可以基于值做一些判断逻辑
// 但是如果中间被改了,则回滚
// conn.Do("SET", "xo", 100)
conn.Send("MULTI")
conn.Send("SET", "xo", v)
r, err = conn.Do("EXEC")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(r)
- 注意事项
- 由于WATCH命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不修改这一键值,所以在一般的情况下我们需要在EXEC执行失败后重新执行整个函数。
- 执行EXEC命令后会取消对所有键的监控,如果不想执行事务中的命令也可以使用
UNWATCH
命令来取消监控。
Redis连接池
package main
import (
"fmt"
"github.com/gomodule/redigo/redis"
"time"
)
var Pool redis.Pool
func init() {
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", 0); err != nil {
c.Close()
return nil, err
}
return c, nil
},
// 从连接池取出连接时,要做的事情
TestOnBorrow: func(c redis.Conn, t time.Time) error {
// t 当前连接被放回pool的时间
// 当前连接放回池子不到一分钟
if time.Since(t) < time.Minute {
return nil
}
// 放回池子超过一分钟的连接,就ping一下
_, err := c.Do("PING")
return err
},
}
}
func someFunc() {
conn := Pool.Get()
defer conn.Close()
res, err := conn.Do("HSET", "user", "name", "lianshi")
fmt.Println(res, err)
res1, err := redis.String(conn.Do("HGET", "user", "name"))
fmt.Printf("res:%s,error:%v\n", res1, err)
}
func main() {
someFunc()
}
知识点
- redis 常用的数据结构底层实现是什么? ziplist、跳跃表
- 缓存雪崩 :设置的缓冲时间相近或者相同,在某个时间点,全部到期了
- 缓存击穿:key到期,然后疯狂访问
- redis优化
key都应该设置缓存时间
一级key不要超过1000个,可以多点设置二级key,一级key:通过key可以直接访问。
bigkey尽量少
- key分级:shop_userinfo_1000、shop_userinfo_1000
- 集群
codis、cluster
- 持久化
AOF: 把每次操作的指令都存起来,然后重新再做一遍
RDB:快照,类似于复制粘贴