Redis实战入门
一. Redis 连接
1. 进入redis环境
docker run -d -p 6379:6379 --name redis redis:latest
docker exec -it redis redis-cli
2. 检测 Redis 是否正常
127.0.0.1:6379> ping
PONG
3. 退出连接
127.0.0.1:6379> quit
[root@VM-4-12-centos data]#
二. redis内容
1. key 操作命令
输入命令 |
含义 |
---|---|
keys * |
查看所有的key |
dbsize |
获取总key数 |
exists key |
查询键是否存在 |
del key |
删除key |
type key |
查询键类型(string,set..) |
ttl key |
查询key的生命周期(秒) |
pttl key |
查询key的生命周期(毫秒) |
expire key |
设置过期时间(秒) |
pexpire key |
设置过期时间(毫秒) |
persist key |
设置永不过期 |
rename key newkey |
更改键名称 |
2. 字符串string 操作命令
输入命令 |
含义 |
举例 |
返回值 |
---|---|---|---|
set key value |
存放键值 |
set java1 test |
OK |
get key |
获取键值 |
get java1 |
"test" |
mset key1 value1 key2 value2 |
批量存放 |
mset java1 1 java2 2 java3 3 |
OK |
mget key1 key2 |
批量获取 |
mget java1 java2 |
1) "test" 2) "2" |
strlen key |
获取值长度 |
strlen java1 |
4 |
append key value |
追加内容 |
append java1 test2 |
(integer) 6 |
getrange key start end |
获取部分内容 |
getrange java1 0 3 |
"1tes" |
3. 无序集合Set 操作命令
相当于 Java 语言里面的 HashSet
语法 |
含义 |
举例 |
返回值 |
---|---|---|---|
sadd key member... |
存储值 |
sadd langs java php c++ go ruby python kotlin java |
(integer) 7 |
smembers key |
获取所有元素语法 |
smembers langs |
1) "php"2) "kotlin"3)"c++"4) "go"5) "ruby"6) "python"7) "java" |
srandmember key count |
随机获取元素语法 |
srandmember langs 3 |
1)"kotlin"2) "python"3) "c++" |
sismember key member |
判断集合是否存在元素 |
sismember langs go |
(integer) 1 |
scard key |
获取集合元素个数 |
scard langs |
(integer) 7 |
srem key member.. |
删除集合元素 |
srem langs java |
(integer) 1 |
spop key count |
弹出元素 |
spop langs 2 |
1) "python" 2) "go" |
4. 有序集合zset
类似于 Java 的 SortedSet 和 HashMap 的结合体
和列表的区别:
1、列表使用链表实现,两头快,中间慢。有序集合是散列表和跳跃表实现的,即使读取中间的元素也比较快。
2、列表不能调整元素位置,有序集合能。
3、有序集合比列表更占内存。
输入命令 |
含义 |
举例 |
返回值 |
---|---|---|---|
|
存储值 |
zadd footCounts 16011 tid 20082 huny 2893 nosy |
3 |
zscore key member |
获取元素分数 |
zscore footCounts tid |
"16011" |
zincrby key increment member |
增加指定元素分数 |
zincrby footCounts 2000 tid |
"18011" |
zcard key |
获取集合元素个数 |
zcard footCounts |
3 |
|
删除指定元素 |
zrem footCounts huny |
1 |
zrank key member |
获取元素排名 |
zrank footCounts tid |
2 |
|
获取指定分数范围排名语法 |
zrangebyscore footCounts 3000 30000 withscores limit 0 1 |
1) "tid"2) "16011" |
zcount key min max |
获取指定范围分数个数 |
zcount footCounts 2000 20000 |
2 |
5. 列表操作命令list
Redis
中的 list
和 Java
中的 LinkedList
很像,底层都是一种链表结构, list
的插入和删除操作非常快,时间复杂度为 0(1),不像数组结构插入、删除操作需要移动数据。
输入命令 |
含义 |
举例 |
返回值 |
---|---|---|---|
lpush key value ... |
左端存值语法 |
lpush list lily sandy |
2 |
rpush key value ... |
右端存值语法 |
rpush list tom kitty |
4 |
lset key index value |
索引存值语法 |
lset list 3 uto |
OK |
lpop key |
左端弹出语法 |
lpop list |
"broad" |
rpop key |
右侧弹出语法 |
rpop list |
"kitty" |
llen key |
获取元素个数 |
llen list |
2 |
lrange key start top |
获取列表元素 |
lrange list 0 -1 |
1) "sandy" 2) "lily"3)"uto" |
index key index |
索引获取语法 |
lindex list 2 |
"ketty " |
lrem key count value |
根据值删除语法 |
lrem userids 0 111 //count=0 删除所有 |
2 |
ltrim key start stop |
范围删除语法 |
ltrim list 2 4 |
OK |
6. 散列表hash
Redis
中的 Hash
和 Java的 HashMap
更加相似, 都是 数组+链表
的结构,当发生 hash 碰撞时将会把元素追加到链表上,值得注意的是在 Redis
的 Hash
中 value
只能是字符串.
输入命令 |
含义 |
举例 |
返回值 |
---|---|---|---|
hset key field value |
存放键值 |
hset user name javastack |
1 |
hmset key field value field value ... |
2 |
hmset user name javastack age 20 address china |
4 |
hsetnx key field value |
不存在时语法 |
hsetnx user tall 180 |
0 |
hget key field |
获取字段值 |
hget key field |
"20" |
hmget key field field ... |
获取多个字段值 |
hmget user name age address |
1) "javastack"2)"20"3) "china" |
hgetall key |
获取所有键与值语法 |
hgetall user |
1) "name"2) "javastack"3) "age"4) "20"5) "address"6) "china" |
hkeys key |
获取所有字段语法 |
127.0.0.1:6379> hkeys user |
1) "name"2) "address"3) "tall"4) "age" |
hvals key |
获取所有值语法 |
hvals user |
1) "javastack"2) "china"3) "170"4) "20" |
hexists key field |
判断字段是否存在 |
hexists user address |
1 |
hlen key |
获取字段数量 |
hlen user |
4 |
hincrby key field increment |
递增/减 |
hincrby user tall -10 |
170 |
hdel key field field ... |
删除字段 |
hdel user age |
1 |
三. key的操作之get set
1. get、set去设置
// exampleClient redis连接客户端的代码
func exampleClient() {
rdb := redis.NewClient(redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
panic(err)
}
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
panic(err)
}
fmt.Println("key", val)
val2, err := rdb.Get(ctx, "key2").Result()
if err == redis.Nil {
fmt.Println("key2 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("key2", val2)
}
}
2. 执行任意命令
go-redis 还提供了一个执行任意命令或自定义命令的 Do 方法,特别是一些 go-redis 库暂时不支持的命令都可以使用该方法执行。具体使用方法如下
// doCommand 基本的get set
func doCommand() {
rdb := redis.NewClient(redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
defer cancel()
// 执行命令获取结果
val, err := rdb.Get(ctx, "key").Result()
fmt.Println("test1", val, err)
// 先获取到命令对象
cmder := rdb.Get(ctx, "key")
fmt.Println("test2", cmder.Val())
fmt.Println("test3", cmder.Err())
// 直接执行命令获取错误
// rdb.Set(ctx, "key", 10, time.Hour).Err()
err = rdb.Set(ctx, "key", 3000, 0).Err()
// 直接执行命令获取值
value := rdb.Get(ctx, "key").Val()
fmt.Println("test4", value)
}
3. redis.Nil
go-redis 库提供了一个 redis.Nil 错误来表示 Key 不存在的错误。因此在使用 go-redis 时需要注意对返回错误的判断。在某些场景下我们应该区别处理 redis.Nil 和其他不为 nil 的错误。
// exampleClient redis连接客户端的代码
func exampleClient() {
rdb := redis.NewClient(redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
panic(err)
}
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
panic(err)
}
fmt.Println("key", val)
val2, err := rdb.Get(ctx, "key2").Result()
if err == redis.Nil {
fmt.Println("key2 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("key2", val2)
}
}
4. 扫描或遍历所有key
你可以使用 KEYS prefix:*
命令按前缀获取所有 key
func ScanKeysDemo2() {
rdb := redis.NewClient(redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
defer cancel()
// 按前缀扫描key
iter := rdb.Scan(ctx, 0, "prefix:*", 0).Iterator()
// 此外,对于 Redis 中的 set、hash、zset 数据类型,go-redis 也支持类似的遍历方法。
// iter := rdb.SScan(ctx, "set-key", 0, "prefix:*", 0).Iterator()
// iter := rdb.HScan(ctx, "hash-key", 0, "prefix:*", 0).Iterator()
// iter := rdb.ZScan(ctx, "sorted-hash-key", 0, "prefix:*", 0).Iterator()
for iter.Next(ctx) {
fmt.Println("[key]:", iter.Val())
}
if err := iter.Err(); err != nil {
panic(err)
}
}
四. zset命令
func ZSetDemo() {
rdb := redis.NewClient(redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
zsetKey := "language_rank"
languages := []*redis.Z{
{Score: 90.0, Member: "Golang"},
{Score: 98.0, Member: "Java"},
{Score: 95.0, Member: "Python"},
{Score: 97.0, Member: "JavaScript"},
{Score: 99.0, Member: "C/C++"},
}
ctx, cancel := context.WithTimeout(context.Background(), 50000*time.Microsecond)
defer cancel()
// ZADD
err := rdb.ZAdd(ctx, zsetKey, languages...).Err()
if err != nil {
fmt.Printf("zadd failed, err:%v\\n", err)
return
}
fmt.Println("zadd success!")
// Goland分数加10
newScore, err := rdb.ZIncrBy(ctx, zsetKey, 10.0, "Golang").Result()
if err != nil {
fmt.Printf("zincrby failed, err:%v\\n", err)
return
}
fmt.Printf("Goland's score is %f now.\\n", newScore)
// 取分数最高的3个
ret := rdb.ZRevRangeWithScores(ctx, zsetKey, 0, 2).Val()
for _, z := range ret {
fmt.Println(z.Member, z.Score)
}
fmt.Println("=================")
// 取95~100分的
op := redis.ZRangeBy{
Min: "95",
Max: "100",
}
ret, err = rdb.ZRangeByScoreWithScores(ctx, zsetKey, op).Result()
if err != nil {
fmt.Printf("zrangebyscore failed, err:%v\\n", err)
return
}
for _, z := range ret {
fmt.Println(z.Member, z.Score)
}
}
执行上面的函数将得到如下输出结果。
zadd success
Golang's score is 100.000000 now.
Golang 100
C/C++ 99
Java 98
Python 95
JavaScript 97
Java 98
C/C++ 99
Golang 100
五. Pipeline
Redis Pipeline 允许通过使用单个 client-server-client 往返执行多个命令来提高性能。区别于一个接一个地执行100个命令,你可以将这些命令放入 pipeline 中,然后使用1次读写操作像执行单个命令一样执行它们。这样做的好处是节省了执行命令的网络往返时间(RTT)。
1. 写法一. pipeline
func PipelineWithNoExecDemo() {
rdb := redis.NewClient(redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
ctx, _ := context.WithTimeout(context.Background(), time.Hour)
var incr *redis.IntCmd
_, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
incr = pipe.Incr(ctx, "pipeline_counter")
pipe.Expire(ctx, "pipeline_counter", time.Hour)
return nil
})
if err != nil {
panic(err)
}
// 在执行pipe.Exec之后才能获取到结果
fmt.Println(incr.Val())
}
也可以使用 Pipelined
方法,它会在函数退出时调用 Exec. 代码会更加简洁
2. 写法二. pipelined
func PipelineWithNoExecMulDemo() {
rdb := redis.NewClient(redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
ctx, _ := context.WithTimeout(context.Background(), time.Hour)
_, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for i := 0; i < 100; i++ {
pipe.Set(ctx, fmt.Sprintf("key%d", i), fmt.Sprintf("key%d", i), time.Hour)
}
return nil
})
if err != nil {
panic(err)
}
cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for i := 0; i < 100; i++ {
pipe.Get(ctx, fmt.Sprintf("key%d", i))
}
return nil
})
if err != nil {
panic(err)
}
for _, cmd := range cmds {
fmt.Println(cmd.(*redis.StringCmd).Val())
}
}
六. 加锁
Redis 是单线程执行命令的,因此单个命令始终是原子的,但是来自不同客户端的两个给定命令可以依次执行,例如在它们之间交替执行。但是, Multi/exec
能够确保在 multi/exec
两个语句之间的命令之间没有其他客户端正在执行命令。
在这种场景我们需要使用 TxPipeline 或 TxPipelined 方法将 pipeline 命令使用 MULTI
和 EXEC
包裹起来。
// ZTransactionTxPipeDemo 这里只是相当于加锁,TxPipeline/TxPipelined
func ZTransactionTxPipeDemo() {
rdb := redis.NewClient(redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
ctx, cancel := context.WithTimeout(context.Background(), 50000*time.Microsecond)
defer cancel()
// TxPipeline demo
pipe := rdb.TxPipeline()
pipe.Incr(ctx, "tx_pipeline_counter")
pipe.Expire(ctx, "tx_pipeline_counter", time.Hour)
_, err := pipe.Exec(ctx)
if err != nil {
panic(err)
}
// TxPipelined demo
var incr2 *redis.IntCmd
_, err = rdb.TxPipelined(ctx, func(pipeliner redis.Pipeliner) error {
incr2 = pipe.Incr(ctx, "tx_pipeline_counter")
pipe.Expire(ctx, "tx_pipeline_counter", time.Hour)
return nil
})
fmt.Println(incr2.Val(), err)
}
七. 事务-watch
我们通常搭配 WATCH
命令来执行事务操作。从使用 WATCH
命令监视某个 key 开始,直到执行 EXEC
命令的这段时间里,如果有其他用户抢先对被监视的 key 进行了替换、更新、删除等操作,那么当用户尝试执行 EXEC
的时候,事务将失败并返回一个错误,用户可以根据这个错误选择重试事务或者放弃事务。
Watch方法接收一个函数和一个或多个key作为参数
// WatchDemo 配合TxPipeline/TxPipelined完成增删改查
func WatchDemo() error {
rdb := redis.NewClient(redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
ctx, cancel := context.WithTimeout(context.Background(), 50000*time.Microsecond)
defer cancel()
key := "key"
return rdb.Watch(ctx, func(tx *redis.Tx) error {
n, err := tx.Get(ctx, key).Int()
if err != nil; err != redis.Nil {
return err
}
// 假设操作耗时5秒
// 5秒内我们通过其他的客户端修改key,当前事务就会失败
time.Sleep(5 * time.Second)
_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Set(ctx, key, n+1, time.Hour)
return nil
})
return err
}, key)
}