首选,如果之前使用过redis容器,我们需要先remove掉之前的容器
让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:域名注册、网络空间、营销软件、网站建设、五台网站维护、网站推广。
然后创建redis容器,并运行
进入redis容器中
接着我们通过 redis-cli 连接测试使用 redis 服务
setex指令 可以设置数据存在的时间, setex key second value
MSET 一次设置多个key-value
MGET一次获取多个key-value
HGET
HGETALL
Hlen和hexist
Lpush 和 Lrange
Lpop和Rpop 从链表取出并移走数据
删除链表所有数据 DEL
字符串无序 不能重复
从连接池中Get出一个conn连接
可以参考: windows环境下redis的安装
启动redis服务器:redis-server.exe redis.windows.conf
获取包:
导入包
访问:
redis事务可以一次执行多个命令, 并且带有以下两个重要的保证:
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
下面介绍golang redis事务用法。
go redis事务常用函数:
TxPipeline - 以Pipeline的方式操作事务
Watch - redis乐观锁支持
1.TxPipeline
以Pipeline的方式操作事务
2.watch
redis乐观锁支持,可以通过watch监听一些Key, 如果这些key的值没有被其他人改变的话,才可以提交事务。
与memcached一样,为了保证效率,数据都是缓存在内存中。
区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。
这使得Redis可执行单层树复制。从盘可以有意无意的对数据进行写操作。
由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。
一、关于连接池
一个数据库服务器只拥有有限的资源,并且如果你没有充分使用这些资源,你可以通过使用更多的连接来提高吞吐量。一旦所有的资源都在使用,那么你就不 能通过增加更多的连接来提高吞吐量。事实上,吞吐量在连接负载较大时就开始下降了。通常可以通过限制与可用的资源相匹配的数据库连接的数量来提高延迟和吞 吐量。
如何在Go语言中使用Redis连接池
如果不使用连接池,那么,每次传输数据,我们都需要进行创建连接,收发数据,关闭连接。在并发量不高的场景,基本上不会有什么问题,一旦并发量上去了,那么,一般就会遇到下面几个常见问题:
性能普遍上不去
CPU 大量资源被系统消耗
网络一旦抖动,会有大量 TIME_WAIT 产生,不得不定期重启服务或定期重启机器
服务器工作不稳定,QPS 忽高忽低
要想解决这些问题,我们就要用到连接池了。连接池的思路很简单,在初始化时,创建一定数量的连接,先把所有长连接存起来,然后,谁需要使用,从这里取走,干完活立马放回来。 如果请求数超出连接池容量,那么就排队等待、退化成短连接或者直接丢弃掉。
二、使用连接池遇到的坑
最近在一个项目中,需要实现一个简单的 Web Server 提供 Redis 的 HTTP interface,提供 JSON 形式的返回结果。考虑用 Go 来实现。
首先,去看一下 Redis 官方推荐的 Go Redis driver。官方 Star 的项目有两个:Radix.v2 和 Redigo。经过简单的比较后,选择了更加轻量级和实现更加优雅的 Radix.v2。
Radix.v2 包是根据功能划分成一个个的 sub package,每一个 sub package 在一个独立的子目录中,结构非常清晰。我的项目中会用到的 sub package 有 redis 和 pool。
由于我想让这种被 fork 的进程最好简单点,做的事情单一一些,所以,在没有深入去看 Radix.v2 的 pool 的实现之前,我选择了自己实现一个 Redis pool。(这里,就不贴代码了。后来发现自己实现的 Redis pool 与 Radix.v2 实现的 Redis pool 的原理是一样的,都是基于 channel 实现的, 遇到的问题也是一样的。)
不过在测试过程中,发现了一个诡异的问题。在请求过程中经常会报 EOF 错误。而且是概率性出现,一会有问题,一会又好了。通过反复的测试,发现 bug 是有规律的,当程序空闲一会后,再进行连续请求,会发生3次失败,然后之后的请求都能成功,而我的连接池大小设置的是3。再进一步分析,程序空闲300秒 后,再请求就会失败,发现我的 Redis server 配置了 timeout 300,至此,问题就清楚了。是连接超时 Redis server 主动断开了连接。客户端这边从一个超时的连接请求就会得到 EOF 错误。
然后我看了一下 Radix.v2 的 pool 包的源码,发现这个库本身并没有检测坏的连接,并替换为新server{location/pool{content_by_lua_block{localredis=require"resty.redis"localred=redis:new()localok,err=red:connect("127.0.0.1",6379)ifnotokthenngx.say("failedtoconnect:",err)returnendok,err=red:set("hello","world")ifnotokthenreturnendred:set_keepalive(10000,100)}}}
发现有个 set_keepalive 的方法,查了一下官方文档,方法的原型是 syntax: ok, err = red:set_keepalive(max_idle_timeout, pool_size) 貌似 max_idle_timeout 这个参数,就是我们所缺少的东西,然后进一步跟踪源码,看看里面是怎么保证连接有效的。
function_M.set_keepalive(self,...)localsock=self.sockifnotsockthenreturnnil,"notinitialized"endifself.subscribedthenreturnnil,"subscribedstate"endreturnsock:setkeepalive(...)end
至此,已经清楚了,使用了 tcp 的 keepalive 心跳机制。
于是,通过与 Radix.v2 的作者一些讨论,选择自己在 redis 这层使用心跳机制,来解决这个问题。
四、最后的解决方案
在创建连接池之后,起一个 goroutine,每隔一段 idleTime 发送一个 PING 到 Redis server。其中,idleTime 略小于 Redis server 的 timeout 配置。连接池初始化部分代码如下:
p,err:=pool.New("tcp",u.Host,concurrency)errHndlr(err)gofunc(){for{p.Cmd("PING")time.Sleep(idelTime*time.Second)}}()
使用 redis 传输数据部分代码如下:
funcredisDo(p*pool.Pool,cmdstring,args...interface{})(reply*redis.Resp,errerror){reply=p.Cmd(cmd,args...)iferr=reply.Err;err!=nil{iferr!=io.EOF{Fatal.Println("redis",cmd,args,"erris",err)}}return}
其中,Radix.v2 连接池内部进行了连接池内连接的获取和放回,代码如下:
//Cmdautomaticallygetsoneclientfromthepool,executesthegivencommand//(returningitsresult),andputstheclientbackinthepoolfunc(p*Pool)Cmd(cmdstring,args...interface{})*redis.Resp{c,err:=p.Get()iferr!=nil{returnredis.NewResp(err)}deferp.Put(c)returnc.Cmd(cmd,args...)}
这样,我们就有了 keepalive 的机制,不会出现 timeout 的连接了,从 redis 连接池里面取出的连接都是可用的连接了。看似简单的代码,却完美的解决了连接池里面超时连接的问题。同时,就算 Redis server 重启等情况,也能保证连接自动重连。
从上一节的内容可知,Do() 和 Receive() 等方法的返回值,除了 error 外,是一个 interface{} 类型的返回值,因此当我们的复杂操作返回的不是基本数据类型时,就需要我们自己解析返回值,例如,当我们利用 HMGET 方法获取一批返回值时,就需要对返回结果进行解析,具体如下:
由于返回值是多条数据,因此需要先将 reply 转成 []interface 类型,然后在遍历结果时在分别转成 []uint8 (byte数组), 最后再转成 string 类型。
随着我们操作复杂度,数据解析的工作量也会非常大,(lua 脚本的使用,会使结果的解析更为复杂,因为可能存在多种类型的结果一起返回的情况,lua 脚本相关的内容会在下一节介绍)。
redigo 包中的返回值助手函数的存在,就是为了帮助我们完成这些枯燥繁琐的数据解析过程。
返回值助手函数相关源码路径为 github.com/gomodule/redigo/redis/reply.go 提供的主要方法如下:
上述返回值助手函数的具体使用,应该依据具体的命令进行选择。如果大家还记得上一节介绍的 Redis 基本数据类型,可能会有些疑问,对于 redis 来说,其数据据存储本质都是 []bytes, 为什么可以解析出 Int、int64、float等类型的数据呢?
我们以 Float64() 为例进行说明,具体源码如下:
其实,返回值助手函数是将 []byte 类型的原始数据,利用 strconv.ParseFloat(string(reply), 64) 转换成了 float64类型,因此在我们使用过程中返回值助手函数的选择,应该基于业务和实际存储的数据格式为依据。我们以第一小节的示例为例,看返回值助手函数如何降低我们的工作量,具体如下:
除了使用返回值助手函数对上述固定结构的结果进行解析外,redigo 包还提供了一个 Scan()函数用于解析自定义的复杂数据结构,我们依然以上一个示例进行说明,具体示例如下:
如果返回结果为结构化切片,也可以使用 canSlice() 方法,从而简化 loop 处理的部分,具体示例如下:
通过上述的示例,我们介绍了 scan 函数的基本用法,但是细心的同学可能会发现吗,为什么数据写入时,value 的类型为 []int64 但是读取时只能按照 string 类型读取呢。这是因为 Redis 底层存储的数据本质都是 string 类型,。 无论是 HMSET 还是 MSET 最终都只能按照 string 类型读取,因为其本质都是 hash 结构,不同之处仅在于 HMSET 是嵌套的 hash类型。 因此,[]int64 数据在写入阶段,就已经被自动处理为 []byte,写入 redis 之后,len 和 类型 属性会丢失。
如果强行按照 []int64解析将出错:
如果 value 必须以结构化的数据存储,那么可以提前对要写入的数据进行编码,例如 json、protobuf 等,取出后再进行解码获得原始数据。