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:快照,类似于复制粘贴