Install
Ubuntu
sudo apt-get update
sudo apt-get install redis
Set password in redis.conf
requirepass YOUR_PASSWORD
Docker
docker run --name redis -d --restart unless-stopped \
-p 6379:6379 \
-v /data/redis:/data \
-v $(pwd)/redis.conf:/usr/local/etc/redis/redis.conf \
redis redis-server /usr/local/etc/redis/redis.conf --appendonly yes
TLS
Genenrate certtificates
# Generate a private key and a self-signed certificate:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365
# Remove the passphrase from the key:
openssl rsa -in key.pem -out key.pem
# Concatenate the private key and the public certificate into a single .pem file
cat key.pem cert.pem > redis.pem
Run redis server with TLS
redis-server \
--port 0 \
--tls-port 6379 \
--tls-cert-file ./redis.pem \
--tls-key-file ./redis.pem \
--tls-ca-cert-file /./cert.pem
Docker with TLS
docker run --name redis -d --restart unless-stopped \
-p 6379:6379 \
-p 6380:6380 \
-v /data/redis:/data \
-v $(pwd)/redis.conf:/usr/local/etc/redis/redis.conf \
-v $(pwd)/tls:/etc/ssl/redis \
redis \
redis-server \
--port 6379 \
--tls-port 6380 \
--tls-cert-file /etc/ssl/redis/redis.pem \
--tls-key-file /etc/ssl/redis/redis.pem \
--tls-ca-cert-file /etc/ssl/redis/cert.pem
Redis Stack
docker run -d --name redis-stack --restart unless-stopped \
-p 6379:6379 \
-p 6380:6380 \
-v /data/redis:/data \
-v $(pwd)/redis.conf:/redis-stack.conf \
-v $(pwd)/tls:/etc/ssl/redis \
-e REDIS_ARGS="--appendonly yes --port 6379 --tls-port 6380 --tls-cert-file /etc/ssl/redis/paas.pem --tls-key-file /etc/ssl/redis/paas.pem --tls-ca-cert-file /etc/ssl/redis/ca.pem" \
redis/redis-stack-server:latest
Redis Insight
docker run -d --name redisinsight --restart unless-stopped \
-p 5540:5540 \
-e RI_PROXY_PATH=/redisinsight \
redis/redisinsight:latest
在 Caddyfile 中增加代理配置
your_host {
handle /redisinsight/* {
reverse_proxy localhost:5540
}
}
Common Commands
# mannual
HELP <command>
# show all keys match a pattern
KEYS <pattern>*
# show type of a key
TYPE key
# show info
INFO
INFO MEMORY
INFO CPU
# database
INFO KEYSPACE # show all databases
DBSIZE # count number of keys
# crashes the Redis server process by performing an invalid memory access
DEBUG SEGFAULT
# monitor
MONITOR
# list clients
CLIENT LIST
# get random key
RANDOMKEY
# expire
SETEX key value EX 60 # expire in 60 seconds
SETEX key value PX 1000 # expire in 1000 milliseconds
EXPIRE key 60 # expire key in 60 seconds
EXPIREAT key 1599191356 # expire key in unix timestamp
PERSIST key # removes the existing timeout
TTL key # checkout remain time to expire
# 切换db
SELECT <index>
MIGRATE
COPY
REPLACE
# auth
AUTH <mysecret>
# shutdown
SHUTDOWN
SHUTDOWN SAVE
SHUTDOWN NOSAVE
String
最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M
常用命令
# set/get
SET key value
GET key
# multiple set/get
MSET key1 value1 key2 value2
MGET key1 key2
# delete
DEL key
# check existence
EXISTS key
# insert if not exists
SETNX key value
# or
SET key value NX
# get length for string
STRLEN key
# set int
SET key 0
# increase/decrease
INCR key
INCRBY key 10
DECR key
DECRBY key 10
内部实现
- int
- SDS(Simple Dynamic String,简单动态字符串)
应用场景
缓存对象
- 可以直接缓存整个对象的 JSON:
SET user:1 '{"name":"xiaolin", "age":18}'
- 采用 MSET 存储各个字段:
MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20
计数
- Redis 处理命令是单线程的,执行命令过程是原子的,因此适合计数场景,例如计算访问次数、点赞、转发数、库存数等
分布式锁
利用 SEX NX
实现锁:SET lock_key unique_value NX PX 10000
- 如果
key
不存在,则插入成功,表示加锁成功 - 如果
key
存在,则插入失败,表示加锁失败 - 一般会设置过期时间,防止 client 异常导致锁无法释放
- 值一般设置为 client 的唯一值,解锁时需要先判断是否是加锁的 client
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
List
List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
列表的最大长度为 $2^{32} - 1$,也即每个列表支持超过 40亿 个元素。
常用命令
# left push to a list
LPUSH list val1
LPUSH list val2 val3 val4
# right push to a list
RPUSH list val1
# left pop
LPOP list
# right pop
RPOP list
# blocking pop, if the List is empty, it waits until there is something to remove.
BLPOP/BRPOP list
# with timeout(second)
BLPOP/BRPOP list 10
# pop from list1 and push to list2
RPOPLPUSH list1 list2
# length of the list
LLEN list
# get the i-th element, 0 is the first. -1 is the last
LINDEX list 0
# get the elements from start to stop (including)
LRANGE list start stop
内部实现
- 双向链表
- quicklist(3.2+)
应用场景
消息队列
- 生产者
LPUSH
消费者RPOP
,可以保证消息有序消费 - 消费者可以使用
BRPOP
阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据,减少性能损失 - 在消息中生成全局唯一 ID,防止消息重复消费
- 可以使用
BRPOPLPUSH
命令备份 List,在异常时重新读取,增加可靠性 - 不支持消费者分组,需要使用 Redis 5.0+ 支持的 Stream 数据类型
Hash
常用命令
# get/set
HSET key field value
HGET key field
# multiple get/set
HMSET key field1 value1 field2 value2
HMGET key field1 field2
# delete field
HDEL key field
# get all fields and values
HGETALL key
# get all fields
HKEYS key
# get all values
HVALS key
# get how many fields in the key
HLEN key
# HSCAN needs to be executed until the returned cursor is 0 in order to retrieve all the fields in a Hash
HSCAN key cursor
# increase `n` to the value of `field` in the key
HINCRBY key field n
内部实现
- 哈希表
应用场景
缓存对象
Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,可以用来存储对象。
HSET uid:1 name Tom age 15
HSET uid:2 name Jerry age 13
HGETALL uid:1
对象也可以用 String + Json 存储,如果对象中有些属性会频繁变化可以考虑用 Hash 类型存储。
购物车
用户 id 为 key,商品 id 为 field,商品数量为 value
- 添加商品:
HSET cart:{用户id} {商品id} 1
- 添加数量:
HINCRBY cart:{用户id} {商品id} 1
- 商品总数:
HLEN cart:{用户id}
- 删除商品:
HDEL cart:{用户id} {商品id}
- 获取购物车所有商品:
HGETALL cart:{用户id}
在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。
Set
Set 是一个无序并唯一的键值集合,一个集合最多可以存储 $2^{32}-1$ 个元素。
常用命令
# add menbers
SADD set1 member1 member2 member3
# remove member
SREM set1 member
# return all members
SMEMBERS set1
# get n random member
SRANDMEMBER set1 n
# check is member or not
SISMEMBER set1 member
# cardinality
SCARD set1
# set operations
# intersetion of sets
SINTER set1 set2 set3
# difference
SDIFF set1 set2 set3
# union
SUNION set1 set2 set3
内部实现
- 哈希表
应用场景
set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。
点赞
可以保证一个用户只能点一个赞,例如 key 是文章id,value 是用户id。
共同关注
可以用来计算共同关注的好友、公众号等。
抽奖活动
存储某活动中中奖的用户名,可以保证同一个用户不会中奖两次。
Zset
Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。
常用命令
# add member with score
ZADD key score member
# get score
ZSCORE key member
# remove members
ZREM key member
# get member number of the key
ZCARD key
# increase score of member
ZINCRBY key increment member
# return sorted members
ZRANGE key start stop
# return reversed sorted
ZREVRANGE key start stop members
# get member's rank
ZRANK key member
# get member's reverse rank
ZREVRANK key member
# get members whose score is in the range, order by score
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
# get members whose score is in the range, order by score, order by dictionary order
ZRANGEBYLEX key min max [LIMIT offset count]
# set operations
# union, add score for the same key
ZUNIONSTORE destkey numberkeys key [key...]
# interset, add score for the same key
ZINTERSTORE destkey numberkeys key [key...]
# no diff operation for zset
内部实现
- 跳表
应用场景
排行榜
学生成绩的排名榜、游戏积分排行榜、视频播放排名、电商系统中商品的销量排名等。
电话、姓名等排序
使用有序集合的 ZRANGEBYLEX
或 ZREVRANGEBYLEX
可以帮助我们实现电话号码或姓名的排序
# sort phone numbers
# add phone number to zset
ZADD phone 0 13100111100 0 13110114300 0 13132110901
# get all numbers
ZRANGEBYLEX phone - +
# get all numbers starts with 132
ZRANGEBYLEX phone [132 (133
# sort names
# add names to zset
zadd names 0 Toumas 0 Jake 0 Bluetuo 0 Gaodeng 0 Aimini 0 Aidehua
# get all names
ZRANGEBYLEX names - +
# get all names starts with A
ZRANGEBYLEX names [A (B
Bitmaps
Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1
的设置,表示某个元素的值或者状态,时间复杂度为O(1)。
由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。
常用命令
# set value(0 or 1)
SETBIT key offset value
# get offset
GETBIT key offset
# count 1's
BITCOUNT key
# count 1's from start to end bits
BITCOUNT key start end
# bitmap operations: AND|OR|XOR|NOT
BITOP [operations] [result] [key1] [keyn…]
# get the position of the first value(0 or 1)
BITPOS [key] [value]
内部实现
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,可以把 Bitmap 看作是一个 bit 数组。
应用场景
用户登录态
一个 key 存储所有用户登录态,用户 ID 作为 offset,50000 万用户只需要 6 MB 的空间。
# login
SETBIT login_status <uid> 1
# check status
GETBIT login_status <uid>
# logout
SETBIT login_status <uid> 0
签到统计
用户+月份为 key,日期为 offset,bit 签到为 1,未签到为 0
# 2022-06-01 用户签到
SETBIT uid:sign:100:202206 0 1
# 2022-06-03 用户签到
SETBIT uid:sign:100:202206 2 1
# 检查 2022-06-03 该用户是否签到
GETBIT uid:sign:100:202206 2
# 统计用户在 6 月的签到次数
BITCOUNT uid:sign:100:202206
连续签到用户数
以 7 天连续打卡为例,每天的日期为 key,用户 ID 为 offset,打卡成功则设置 bit 为 1,一共维护 7 个 Bitmap。可以给 Bitmap 设置过期时间让其自动删除。假设一天有一亿个用户,一个 Bitmap 大约占 12 MB 的内存($10^8/8/1024/1024$),7 天的 Bitmap 的内存开销约为 84 MB。
对这 7 个 Bitmap 的对应的 bit 位做 『与』运算。同样的 UserID offset 都是一样的,当一个 userID 在 7 个 Bitmap 对应对应的 offset 位置的 bit = 1 就说明该用户 7 天连续打卡。
# 将三个 bitmap 进行 AND 操作,并将结果保存到 destmap 中
BITOP AND destmap bitmap:01 bitmap:02 bitmap:03
# 统计 bit 位 = 1 的个数
BITCOUNT destmap
HyperLogLog
HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。
常用命令
# 添加指定元素到 HyperLogLog 中
PFADD key element [element ...]
# 返回给定 HyperLogLog 的基数估算值。
PFCOUNT key [key ...]
# 将多个 HyperLogLog 合并为一个 HyperLogLog
PFMERGE destkey sourcekey [sourcekey ...]
内部实现
- HyperLogLog
应用场景
百万级网页 UV 计数
计算 $2^{64}$ 个元素的集合的 Cardinality ,只需要 12 KB 的内存。
# 将访问的用户加入 page 的 HyperLogLog
PFADD page1:uv user1 user2 user3 user4 user5
# 计算 UV,即 user 的 Cardinality
PFCOUNT page1:uv
Geo
常用命令
# 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中
GEOADD key longitude latitude member [longitude latitude member ...]
# 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
GEOPOS key member [member ...]
# 返回两个给定位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]
# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
内部实现
- GeoHash 编码后放到 zset 中,编码值作为 score,这样就能按照编码范围搜索
应用场景
叫车服务
# add car location to GEO
GEOADD cars:locations 116.034579 39.030452 <card_id>
# search cars in 5 km by lat/lon
GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10
Stream
常用命令
# 插入消息,保证有序,可以自动生成全局唯一 ID
# * 表示让 Redis 为插入的数据自动生成一个全局唯一的 ID
XADD <mq_name> * name Tom
# 插入成功后会返回全局唯一 ID
# 读取消息
# 从最开始读取 3 条消息
XREAD COUNT 3 STREAMS <mq_name> 0
# 可以同时消费多个队列
XREAD COUNT 3 STREAMS <mq_name_1> <mq_name_2> 0
# 从指定 ID 开始消费,自动生成的 ID 的格式为 时间戳-序号
XREAD COUNT 3 STREAMS <mq_name> 1538561698944-0
# 异步消费消息并指定超时时间,`$` 表示只等待开始消费之后生产的消息
XREAD block 10000 Stream <mq_name> $
# 不设置超时时间
XREAD block 0 Stream <mq_name> $
# 以消费组的形式消费
# create group
XGROUP create <mq_name> <group_name> 0
# 按消费组形式读取消息
XREADGROUP group <group_name> <consumer_name> Stream <mq_name>
# 查询每个消费组内所有消费者已读取但尚未确认的消息
XPENDING <mq_name> <group_name>
# 向消息队列确认消息处理已完成
XACK <mq_name> <group_name> <message_id>
应用场景
消息队列
- 通过 PENDING List 和
XACK
可以保证生产者/消费者不丢数据 - Redis 本身可能丢数据
- AOF 持久化配置为每秒写盘,但这个写盘过程是异步的,Redis 宕机时会存在数据丢失的可能
- 主从复制也是异步的,主从切换时,也存在丢失数据的可能
- Redis 的数据都存储在内存中,这就意味着一旦发生消息积压,则会导致 Redis 的内存持续增长,如果超过机器内存上限,就会面临被 OOM 的风险
- Stream 提供了可以指定队列最大长度的功能
Pub/Sub
PUBLISH channel message
SUBSCRIBE channel
UNSUBSCRIBE channel
PSUBSCRIBE pattern* # subscribe channels by pattern
PUNSUBSCRIBE pattern* # unsubscribe channels by pattern
# show all active channels by pattern
PUBSUB CHANNELS pattern*
# count subscribers of channel
PUBSUB NUMSUB channels
#
PUBSUB NUMPAT
应用场景
消息队列
- 旧版本中 subscriber 消费不及时会导致 Redis 内存过大
- 新版本可以配置
client-output-buffer-limit
参数断开有积压的 client
- 新版本可以配置
- 在客户端断开时 publish 的消息无法被该客户端消费到
Transaction
MULTI # start a transaction
EXEC # exucute the transaction
DISCARD # discard commands since MULTI
WATCH
UNWATCH
Sentinel
Sentinel commands
# show info
INFO
# get configs
SENTINEL CONFIG GET *
# get all masters
SENTINEL MASTERS
# get current sentinel master
SENTINEL MASTER <your cluster id>
# get current sentinel master IP
SENTINEL GET-MASTER-ADDR-BY-NAME <your cluster name>
SDK
Python client
Install
pip install redis
Usage
import redis
client = redis.Redis(host=RD_HOST, port=RD_PORT, db=RD_DB, password=RD_PASS)
# hset
res = rd.hset(name, key, value)
# hmset
res = rd.hset(name, mapping=data_dict)
# subscribe
sub = r.pubsub()
sub.subscribe(redis_channels)
try:
for msg in sub.listen():
if msg['type'] == 'message':
print(f"received message from {msg['channel']}: {msg['message']}")
handle_massage(msg)
finally:
sub.close()
Node.js
node-redis
const client = redis.createClient({
url: `redis://${REDIS_USER}:${REDIS_PASS}@${REDIS_HOST}:${REDIS_PORT}`,
socket: {
tls: true,
ca: fs.readFileSync('/etc/ssl/ca.pem'),
cert: fs.readFileSync('/etc/ssl/cert.pem'),
key: fs.readFileSync('/etc/ssl/key.pem'),
}
});
client.on('error', err => console.log('Redis Client Error', err));
await client.connect();
await client.set('key', 'value');
const value = await client.get('foo');
console.log(value);
ioredis
GUI
- WebUI
- Redis Insight