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

redis_hash_shopping_cart.png

  • 添加商品: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

内部实现

  • 跳表

应用场景

排行榜

学生成绩的排名榜、游戏积分排行榜、视频播放排名、电商系统中商品的销量排名等。

电话、姓名等排序

使用有序集合的 ZRANGEBYLEXZREVRANGEBYLEX 可以帮助我们实现电话号码或姓名的排序

# 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 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景

redis_bitmap_internal.png

常用命令

# 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

Github - ioredis

GUI

  • WebUI
    • Redis Insight

References