这是我参与「第五届青训营 」伴学笔记创作活动的第15天。
学习了Redis入门的基础命令与使用。
基本介绍
Redis是NoSQL数据库,而不是传统的关系型数据库。
全称:Remote Dictionary Server,性能很高,单机能够达到15w qps,通常适合做缓存,也可以做持久化。
是完全开源免费且高性能的(Key/Value)分布式数据库,基于内存运行并支持持久化的NoSQL数据库,是最热门的NoSQL数据库之一,也称为数据结构服务器。
一般监听的端口:6379
运作机制简要概括是:代码发出指令(用客户端敲命令也可以啦),核心组件进行处理。
redis核心组件功能
- 解析指令
- 做相应数据操作处理
内存的五种数据结构
- string、key-val
- hash
- list
- set
- zset(有序集合)
基本使用
Redis默认有16个数据库,初始默认使用0号库。
- key-val [set]
- 查看当前redis的所有key [key *]
- 获取key对应的值 [get key]
- 切换redis数据库 [select index]
- 如何查看当前数据库的key-val数量 [dbsize]
- 清空当前数据库的key-val和清空所有数据库的key-val [flushdb flushall]
Redis数据类型和CRUD
Redis五种数据类型:String、hash、list、set、zset。
String
- 是redis最基本的数据类型,一个key对应一个value。
- String是二进制安全的,除普通的字符串外,也可以存放图片等数据(但一般不会这么存的,因为图片大部分很大)。
- redis中字符串value最大是512M
举例:存放于一个地址信息
- address 北京天安门
- key -> address
- value -> 北京天安门
CRUD
-
set
- 如果存在就相当于修改,不存在就是添加
- set key value
-
setex(set with expire)
- 实现定时任务,在设定的时间后redis就会把该值从库中删除
- setex mess01 10 hello
-
mset
- 同时设置一个或多个key-value对
- mset key1 value1 key2 value2
-
mget
- 同时获取多个键值
- mget key1 key2
Hash
- Redis hash是一个键值对集合。类似于:var user1 map[string]string
- Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象,注意key不能重复
CRUD
添加
- hset key key-key value -> 一次性设置一个键值
- hmset key key-key1 value1 key-key2 value2 key-key3 value3 -> 一次性设置多个键值
需要注意的是,如设置的键或值中包括空格,一定要用"",比如"golang coder"
获取
- hget key key-keyx
- hgetall -> 获取全部键值
- hmget key key-keyx key-keyy -> 一次性获取指定键值
删除
- hdel key -> 会删掉该键后的所有键
- hlen key -> 统计一个hash中有几个元素
- hexists key key-keyx -> 判断是否有某个字段,1为有,0为没有
List
- 是简单的字符型的列表,按照插入的顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边)
- List本质是一个链表,List的元素是有序的,元素的值可以重复
CRUD
添加
- lpush key value1 value2 value3.. -> 从头部插入
- rpush -> 从尾部插入
查询
遵循后进先出,比如在上述添加中,查询到的第一个元素为value3
- lrange city 0 -1 -> 全部查询
- lrange key start stop -> 返回列表指定区间的元素
弹出(删除)
- lpop -> 从头部弹出数据
- lpop key
- rpop -> 从尾部弹出数据
- rpop key
llen key -> 返回列表的长度
Set
- 是string类型的无序集合
- 底层是HashTable数据结构,Set也是存放很多字符串元素,字符串元素是无序的,而且元素的值不能重复
CRUD
添加
- sadd key value1 value2...
查询
- smembers key
- sismemeber key value -> 判断value是否存在,有为1,无为0
删除
- srem key value -> 删除指定的value,删除成功为1,失败为0
Go连接与操作Redis
安装第三方包
我拉的是这个:go get github.com/garyburd/redigo/redis
操作String类型
func StringOperate() {
c, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("connect redis fail")
return
}
defer c.Close()
_, err = c.Do("set", "key1", 666)
if err != nil {
fmt.Println(err)
}
// 返回的r是空接口,而key1是字符串,故要转换
r, err := redis.String(c.Do("get", "key1"))
if err != nil {
fmt.Println("get key1 failed")
return
}
fmt.Println(r)
}
操作Hash类型
func HashOperate() {
c, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("connect redis fail")
return
}
defer c.Close()
_, err = c.Do("hmset", "user01", "name", "jack", "age", 30)
if err != nil {
fmt.Println(err)
}
// 返回的r是空接口,而key1是字符串,故要转换
r, err := redis.Strings(c.Do("hgetall", "user01"))
if err != nil {
fmt.Println("get key1 failed", err)
return
}
for i, v := range r {
fmt.Printf("r[%v]=%v\n", i, v)
}
}
其实,无论操作什么类型,在DO()中写原生的redis命令,命令行中的空格在代码中用""与,区分和分隔来即可,需要注意的是在获取数据的时候要判断好类型。
连接池
连接池是其中有一些不关闭的连接(连接数据库),当客户端要使用redis时,直接在连接池中取一个连接用,使用完成后再返回连接池。
流程
- 事先初始化一定数量的连接,放入到连接池
- 当需要操作redis时,直接从redis连接池中取出连接即可
- 由此,节省了临时获取redis连接的时间,提升了效率
Demo
var Pool *redis.Pool
func Init() {
Pool = &redis.Pool{
MaxIdle: 8, // 最大空闲连接数
MaxActive: 0, // 表示和数据库的最大连接数,0表示没有限制
IdleTimeout: 100, // 最大空闲时间,如果超过这个时间连接都没有被使用,则会返回连接池
Dial: func() (redis.Conn, error) { //初始化连接池的代码
return redis.Dial("tcp", "127.0.0.1:6379")
},
}
}
func main() {
conn := Pool.Get()
defer conn.Close()
_, err := conn.Do("set", "name", "durant")
if err != nil {
fmt.Println("coon.Do err=", err)
return
}
r, err := redis.String(conn.Do("Get", "name"))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(r)
}
go-redis
由于后来发现开发中常用的是go-redis,故做一次更新,以下是go-redis的一些常用操作。
获取go-redis
go get github.com/go-redis/redis/v8
初始化
var rdb *redis.Client
func init() {
rdb = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
}
String类型
GetSet
打印key的旧值,但会同时把新值存入
oldVal, err := rdb.GetSet(ctx, "name", "rose").Result()
if err != nil {
panic(err)
}
fmt.Println("key", oldVal)
SetNx
如果Key不存在,则设置这个key值,若存在则不会进行操作
err := rdb.SetNX(ctx,"name","jack",0).Err()
if err != nil {
panic(err)
}
MGet
批量查询
vals, err := rdb.MGet(ctx, "name", "name1", "name2").Result()
if err != nil {
panic(err)
}
fmt.Println(vals)
MSet
批量设置
Incr,IncrBy(针对integer)
Incr函数每次加一
val, err := rdb.Incr(ctx, "key").Result()
if err != nil {
panic(err)
}
fmt.Println("最新值", val)
IncrBy函数,可以指定每次递增多少
valBy, err := rdb.IncrBy(ctx, "key", 2).Result()
if err != nil {
panic(err)
}
fmt.Println("最新值", valBy)
IncrByFloat
可以指定每次递增多少,跟IncrBy的区别是累加的是浮点数
valBy, err := rdb.IncrByFloat(ctx, "key", 2.2).Result()
if err != nil {
panic(err)
}
fmt.Println("最新值", valBy)
Decr
递减函数相关,用法和函数命名与Incr类似,不再赘述
Del
删除Key操作,支持批量删除
rdb.Del(ctx, "key")
删除多个key
err := rdb.Del(ctx, "name", "name1", "name2").Err()
if err != nil {
panic(err)
}
Expire
设置Key的过期时间
rdb.Expire(ctx, "key", 3 * time.Second)
当然在Set的时候也可以设置这个参数的,如果为0则为不删除即持久化。
Hash类型
HSet
err := rdb.HSet(ctx, "user1", "name", "durant", "hobby", "basketball", "count", 0).Err()
if err != nil {
panic(err)
}
HGet
username, err := rdb.HGet(ctx, "user1", "name").Result()
if err != nil {
panic(err)
}
fmt.Println(username)
HGetAll
data, err := rdb.HGetAll(ctx, "user1").Result()
HincrBy
根据Key和Field字段,累加字段的数据
例:累加count字段的值
count, err := rdb.HIncrBy(ctx, "user1", "count", 2).Result()
if err != nil {
panic(err)
}
fmt.Println(count)
HKeys
根据Key返回所有字段名,返回的keys是一个String数组
keys, err := rdb.HKeys(ctx, "user1").Result()
if err != nil {
panic(err)
}
fmt.Println(keys)
HLen
根据Key,查询Hash的字段数量
size, err := rdb.HLen(ctx, "user1").Result()
if err != nil {
panic(err)
}
fmt.Println(size)
List
LRem
删除列表中的数据
例:从列表左边开始,删除100,如果出现重复元素,仅删除1次,也就是删除第一个
dels, err := rdb.LRem(ctx,"key", 1,100).Result()
if err != nil {
panic(err)
}
例:如果存在多个100,则从列表左边开始删除2个100
rdb.LRem(ctx,"key",2,100)
例:如果存在多个100,则从列表右边开始删除2个100
rdb.LRem(ctx,"key",-2,100)
LInsert
在指定位置插入数据
例:在列表中5的前面插入4
err := rdb.LInsert(ctx,"key","before",5,4).Err()
if err != nil {
panic(err)
}
例:在列表中zhangsan元素的前面插入 欢迎你
rdb.LInsert(ctx,"key","before","zhangsan","欢迎你")
例:在列表中zhangsan元素的后面插入 2022
rdb.LInsert(ctx,"key","after","zhangsan","2022")
Zset
ZAdd
添加一个或者多个元素到集合,如果元素已经存在则更新分数
例:添加一个元素到集合中,这个元素的分数是2.5,元素名是zhangsan
err := rdb.ZAdd(ctx, "key", &redis.Z{Score: 2.5, Member: "zhangsan"}).Err()
if err != nil {
panic(err)
}
ZCard
返回集合元素个数
size, err := rdb.ZCard(ctx,"key").Result()
if err != nil {
panic(err)
}
fmt.Println(size)
}
ZCount
统计某个分数范围内的元素个数
例:返回:1 <= 分数 <=5 的元素个数,注意:"1","5"两个参数是字符串
size, err := rdb.ZCount(ctx, "Key","1","5").Result()
if err != nil {
panic(err)
}
fmt.Println(size)
例:返回:1 < 分数 <= 5的元素个数
说明:默认第二,第三个参数是大于等于和小于等于的关系 如果加上 ( 则表示大于或者小于,相当于去掉了等于关系。
size, err := rdb.ZCount(ctx, "key","(1","5").Result()
ZRangeByScore
根据分数范围返回集合元素,元素根据分数从小到大排序,支持分页
// 初始化查询条件,Offset和Count用于分页
op := redis.ZRangeBy{
Min: "2", // 最小分数
Max: "10", // 最大分数
Offset: 0, // 类似sql的LIMIT,表示初始偏移量
Count: 5, // 一次返回多少数据
}
vals, err := rdb.ZRangeByScore(ctx, "key", &op).Result()
if err != nil {
panic(err)
}
for _, val := range vals {
fmt.Println(val)
}
ZRangeByScoreWithScores
用法跟ZRangeByScore一样,区别是除了返回集合元素,同时也返回元素对应的分数
ZScore
查询对应的分数
ZRank
根据元素名,查询集合元素在集合中的排名,从0开始算,集合元素按分数从小到大排序
发布订阅
subscriber
Redis提供了发布订阅功能,可以用于消息的传输,Redis的发布订阅机制包括三个部分,发布者、订阅者和Channel。
发布者和订阅者都是Redis客户端,Channel则为Redis服务器端,发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能收到这条消息。
publisher
package main
import (
"context"
"github.com/go-redis/redis/v8"
)
func init() {
rdb = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
}
func publish() {
ctx := context.Background()
rdb.Publish(ctx, "channel1", "msg")
}
两者通信
package main
import (
"context"
"fmt"
)
func subscribe() {
ctx := context.Background()
sub := rdb.Subscribe(ctx, "channel1")
// 扫描方式1
//for ch := range sub.Channel(){
// fmt.Println(ch.Channel) // 名称
// fmt.Println(ch.Payload)
//}
// 扫描方式2
for {
message, err := sub.ReceiveMessage(ctx)
if err != nil {
panic(err)
}
fmt.Println(message.Channel) // 名称
fmt.Println(message.Payload)
}
}
事务处理
TxPipeline
redis事务可以一次性执行多个命令,并且带有以下两个重要的保证:
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部不执行
func main() {
ctx := context.Background()
// 开启一个Pipeline的方式操作事务
pipe := rdb.TxPipeline()
// 执行事务操作,可以通过pipe读写redis
incr := pipe.Incr(ctx, "tx_pipeline_counter")
pipe.Expire(ctx, "tx_pipeline_counter", time.Hour)
// 上述代码等同于执行下面的Redis命令
// MUL TI
// INCR pipeline_counter
// EXPIRE pipeline_counts 3600
// EXEC
// 通过Exec函数提交Redis事务,提交后才会执行
_, err := pipe.Exec(ctx)
// 提交事务后,可以查询事务操作的结果
// 前面执行的Incr函数,在没有执行exec函数之前,实际上还没开始运行
fmt.Println(incr.Val(), err)
}
Watch
redis乐观锁支持,可以通过watch监听一些Key,如果这些Key的值没有被其他人改变的话,才可以提交事务。
// 定义一个回调函数,用于处理事务逻辑
func main() {
fn := func(tx *redis.Tx) error {
// 先查询当前watch监听的key的值
v, err := tx.Get(ctx, "key").Int()
if err != nil && err != redis.Nil {
return err
}
// 这里可以处理业务
v++
// 只有key的值没有改变的情况下,Pipelined函数才会调用成功
_, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error {
// 这里给key设置最新值
pipe.Set(ctx, "key", v, 0)
return nil
})
return err
}
// 使用Watch监听一些key,同时绑定一个回调函数fn,监听Key后的逻辑在写在fn这个回调函数里面
// 如果想监听多个key,可以这么写:client.Watch(ctx, fn, "key1", "key2", "key3")
rdb.Watch(ctx, fn, "key")
}
以上内容若有不正之处,恳请您不吝指正!