redis 基础
所有数据都以唯一 key 字符串作为名称,而 value 只是数据类型的差异。所以,针对 key 的命令都是通用的。
方便演示,采用 docker 镜像,可以选择 redis:latest 镜像,这里我选择了带布隆过虑器的 redis 镜像。
docker run -p 6379:6379 --name redis -d redislabs/rebloom:latest
docker exec -it redis
查看一下启动服务的命令帮助
redis-server --help
Usage: ./redis-server [/path/to/redis.conf] [options] [-]
./redis-server - (read config from stdin)
./redis-server -v or --version
./redis-server -h or --help
./redis-server --test-memory <megabytes>
./redis-server --check-system
Examples:
./redis-server
./redis-server /etc/redis/6379.conf
echo 'maxmemory 128mb' | ./redis-server -
./redis-server --port 7777 --replicaof 127.0.0.1 8888
./redis-server /etc/myredis.conf --loglevel verbose -
Sentinel mode:
./redis-server /etc/sentinel.conf --sentinel
由于 docker 容器运行时,自动启用了 redis 服务,所以下面,我们直接连接即可:
redis-cli
127.0.0.1:6379>
# 查看帮助
help
redis-cli 6.2.5
To get help about Redis commands type:
"help @<group>" to get a list of commands in <group>
"help <command>" for help on <command>
"help <tab>" to get a list of possible help topics
"quit" to exit
To set redis-cli preferences:
":set hints" enable online hints
":set nohints" disable online hints
Set your preferences in ~/.redisclirc
# 查看string 类型相关命令
help @string
# 查看 set 命令帮助
help set
# tab 自动补全命令
help se<tab>
# 启用在线帮助
:set hints
# 关闭在线帮助
:set nohints
key 操作命令:
del 删除
dump 序列化
exists 是否存在
expire 过期时长
expireat 过期时间点(timestamp)
keys 正则表达式搜索
migrate 迁移
move 移库,不会覆盖
object 从内部察看给定 key 的 Redis 对象, 它通常用在除错(debugging)或者了解为了节省空间而对 key 使用特殊编码的情况。
object refcount 返回给定 key 引用所储存的值的次数。此命令主要用于除错。
object encoding 返回给定 key 锁储存的值所使用的内部表示(representation)。
object idletime 返回给定 key 自储存以来的空闲时间(idle, 没有被读取也没有被写入),以秒为单位。
persist 清除过期时间
pexpire 毫秒级的过期时长
pexpireat 毫秒级的过期时间点
pttl 毫秒级的剩余生存时间
randomkey 随机返回一个 key
rename 重命名,新名覆盖
renamenx 安全的重命名(不覆盖)
restore 反序列化给定的序列化值
scan 迭代当前数据库中的 key
sort 排序
touch 修改指定 key 最后访问时间
ttl 查看剩余存活时长
type 查看类型
unlink 异步删除
wait 阻塞当前客户端,直到所有以前的写命令都成功的传输和指定的slaves确认
server 命令:
config get 查看当前配置项,config get * 可查看全部配置项
config set 设置配置项值
5种基础数据类型:string, list, hash, set, zset
复合类数据结构,当最后一个元素被删除,数据结构被自动删除,回收内存。
设置过期时间,是以 key 为单位的,即:复合类整体过期,而不是其中的某个元素过期。
string 字符串
最简单的数据结构,它是一个字符数组。可以动态扩容,采用预留冗余空间的方式来减少内存的频繁分配。字符串小于 1M 时,采用加倍扩容,超过时,最多扩容 1M。
注意,用 set 命令重新设置 key 时,会清除设置的 ttl 时长。
127.0.0.1:6379> set name codehole
OK
127.0.0.1:6379> get name
"codehole"
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> mset name1 boy name2 girl name3 tom
OK
127.0.0.1:6379> mget name1 name2 name3 name4
1) "boy"
2) "girl"
3) "tom"
4) (nil)
127.0.0.1:6379> set name codehole
OK
127.0.0.1:6379> expire name 5
(integer) 1
127.0.0.1:6379> ttl name
(integer) 3
127.0.0.1:6379> setex name 5 codehole
OK
127.0.0.1:6379> setex name 5 codehole
OK
127.0.0.1:6379> ttl name
(integer) 4
127.0.0.1:6379> setnx name holycoder
(integer) 1
127.0.0.1:6379> set age 10
OK
127.0.0.1:6379> incr age
(integer) 11
127.0.0.1:6379> incrby age 10
(integer) 21
127.0.0.1:6379> incrby age -5
(integer) 16
list 列表
它是链表,插入和删除非常快,时间复杂度O(1),但索引定位很慢时间复杂度O(n),列表每个元素都使用双向指针串连,支持左右双向遍历。
左进右出或右进左出为队列,右进右出或左进左出为栈。
127.0.0.1:6379> rpush books python java golang
(integer) 3
127.0.0.1:6379> llen books
(integer) 3
127.0.0.1:6379> lpop books
"python"
127.0.0.1:6379> lpop books
"java"
127.0.0.1:6379> rpop books
"golang"
127.0.0.1:6379> llen books
(integer) 0
127.0.0.1:6379> lpush books java python golang
(integer) 3
127.0.0.1:6379> lindex books 1
"python"
127.0.0.1:6379> lrange books 0 -1
1) "golang"
2) "python"
3) "java"
127.0.0.1:6379> ltrim books 1 -1
OK
127.0.0.1:6379> lrange books 0 -1
1) "python"
2) "java"
hash 字典
它是无序字典,内部存储很多键值对。实现结构是数组 + 链表,通过计算 field 的 hash 值,找到数组中的索引位置,存放为链表结构。碰撞的元素追加到链表尾。
虽然 hash 比字符串占用更多存储空间,但 hash 可以获取单个字段值,而 json 字符串只能整个获取。
127.0.0.1:6379> hset books java "think in java"
(integer) 1
127.0.0.1:6379> hset books python "python cookbook"
(integer) 1
127.0.0.1:6379> hgetall books
1) "java"
2) "think in java"
3) "python"
4) "python cookbook"
127.0.0.1:6379> hlen books
(integer) 2
127.0.0.1:6379> hdel books python
(integer) 1
127.0.0.1:6379> hget books python
(nil)
127.0.0.1:6379> hget books java
"think in java"
127.0.0.1:6379> hmset books python "cook python book"
OK
127.0.0.1:6379> hmset books python "cook python book" php "php book"
OK
127.0.0.1:6379> hset user age 29
(integer) 1
127.0.0.1:6379> hincrby user age 1
(integer) 30
set 集合
它内部实现相当于特殊的字典,字典中所有 value 都是 null 值,field 是无序且唯一的。
127.0.0.1:6379> sadd books python
(integer) 1
127.0.0.1:6379> sadd books python
(integer) 0
127.0.0.1:6379> sadd books java golang
(integer) 2
127.0.0.1:6379> smembers books
1) "python"
2) "golang"
3) "java"
127.0.0.1:6379> sismember books java
(integer) 1
127.0.0.1:6379> scard books
(integer) 3
127.0.0.1:6379> spop books
"java"
zset 有序集合
集合中的元素带有 score,根据 score,对元素进行排序。比如:学生成绩,以学号为 member,以成绩为 score
127.0.0.1:6379> zadd books 9.0 "think in java"
(integer) 1
127.0.0.1:6379> zadd books 9.0 "java cookbook"
(integer) 1
127.0.0.1:6379> zrange books 0 -1
1) "java cookbook"
2) "think in java"
127.0.0.1:6379> zrevrange books 0 -1
1) "think in java"
2) "java cookbook"
127.0.0.1:6379> zcard books
(integer) 2
127.0.0.1:6379> zscore books "think in java"
"9"
127.0.0.1:6379> zrank books "think in java"
(integer) 1
127.0.0.1:6379> zrangebyscore books 0 8.91
(empty array)
127.0.0.1:6379> zrem books "think in java"
(integer) 1
HyperLogLog
不怎么精确的去重计数方案。
127.0.0.1:6379> pfadd books java php python
(integer) 1
127.0.0.1:6379> pfadd books lua javascript java
(integer) 1
127.0.0.1:6379> pfcount books
(integer) 5
127.0.0.1:6379> pfadd books2 aaa bbb ccc
(integer) 1
127.0.0.1:6379> pfcount books2
(integer) 3
127.0.0.1:6379> pfmerge books books2
OK
127.0.0.1:6379> pfcount books
(integer) 8
布隆过虑器
布隆过虑器就像一个不怎么精确的 set 结构,当你用 contains 方法判断某个元素是否存在时,它可能会误判。但只要参数设置合理,精度也能满足需求。
当它判定不存在时,一定不存在,反之未然。之所以使用它,是因为非常省内存,参节省90%以上的内存。
它的原理是:有一个大型的位数组和几个无偏 hash 函数(计算的 hash 值分布均匀),每个 hash 函数分别对元素进行 hash 运算,得到的各个 hash 值。再用各个 hash 值分别对位数组长度取模,得到各个位置。
把这些位置上的值都置为1,这样就完成了添加操作。判断是否存在时,就判断这些位置上是否都有值。
因此,判断为存在时,可能是多个元素的所有落点,正好占满了所需要的位置,造成误判。因为作者叫 Bloom Filter 所以命令是以 bf 开头的
127.0.0.1:6379> bf.add codehole user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user1
(integer) 1
127.0.0.1:6379> bf.exists codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user5
(integer) 0
127.0.0.1:6379> bf.madd codehole user4 user5 user6
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5
1) (integer) 1
2) (integer) 1
发布订阅
subscribe <channel> ... 订阅
unsubscribe <channel> ... 取消订阅
psubscribe news.* 模式匹配订阅
punsubscribe news.* 取消模式匹配订阅
publish <channel> <message> 发布
分布式锁:
通过 set 命令设置一个字符串值,key 与业务相关,value 为随机生成,ex 过期时间。value 随机生成,删除锁时,对比一下当前持有的 value 是否一致,以保证多线程环境下,获取和释放锁为同一线程。释放锁时,匹配和删除 key 不是原子操作,可以用 lua 脚本实现多个指令原子性执行,但也只是相对安全。锁超时是个很麻烦的问题,所以要实现可重入锁,即,在代码执行快超时的时候,再加一次锁。总之,尽量不要用于执行较长时间的任务。
set key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX]
set look:<name> <random> ex <second>
消息队列
可用 list 结构实现,lpush/rpush 放入列表,获取队列元素时, 用阻塞读的方式(blpop/brpop) 代替空轮询。但阻塞太久会遇到空闲连接自动断开的问题,所以使用阻塞读时,注意捕获异常,并重试。
延时队列
可以用 zset 实现,消息体为 value,到期处理时间为 score,注意确保消息被多次消费不会产生问题。
zrangebyscore 取到一个消息,多个进程可能同时取到消息,再调用 zrem 删除消息,如果当前进程删除成功,表示抢到了任务,继而进行后续业务流程,没抢到的进程,就再循环下一遍。
为了优化性能,建议把 zrangebyscore 和 zrem 用 lua 脚本打包成原子操作,从而避免多进程争抢浪费资源。