如何学习redis?
- 了解redis是什么
- redis如何使用,如何操作redis中的数据结构
- 典型应用和操作
- 阅读redis源码
一、Redis
Redis 是Remote Dictionary Service 的简称;也是远程字典服务;
Redis 是内存数据库,KV 数据库,数据结构数据库;
Redis 应用非常广泛,如Twitter、暴雪娱乐、Github、Stack Overflow、腾讯、阿里巴巴、京
东、华为、新浪微博等,很多中小型公司也在使用;
Redis 命令查看:http://redis.cn/commands.html
二、应用
- 记录朋友圈点赞数、评论数和点击数(hash)
- 记录朋友圈说说列表(排序),便于快速显示朋友圈(list)
- 记录文章的标题、摘要、作者和封面,用于列表页展示(hash)
- 记录朋友圈的点赞用户ID列表,评论ID列表,用于显示和去重计数(zset)
- 缓存热点数据,减少数据库压力(hash)
- 如果朋友圈说说 ID 是整数 id,可使用 redis 来分配朋友圈说说 id(计数器)(string)
- 通过集合(set)的交并差集运算来实现记录好友关系(set)
- 游戏业务中,每局战绩存储(list)
三、安装和编译
git clone https://gitee.com/mirrors/redis.git -b 6.2 cd redis make make test make install # 默认安装在 /usr/local/bin # redis-server 是服务端程序 # redis-cli 是客户端程序
启动
mkdir redis-data # 把redis文件夹下 redis.conf 拷贝到 redis-data # 修改 redis.conf # requirepass 修改密码 123456 # daemonize yes cd redis-data redis-server redis.conf # 通过 redis-cli 访问 redis-server redis-cli -h 127.0.0.1 -a 123456
如果是当前主机使用
redis-server redis.conf redis-cli auth "123456"
四、认识redis
Redis是内部数据库,kv数据库,数据结构数据库
有string,hash,list,set,zset
server向redis发送命令,来操作redis中的数据结构,redis应答操作结果
redis默认有16个数据库
redis只能单线程使用,同时只能使用一个数据库,用字典的方式组织起来,而mysql使用b+树组织的
redis中的数据结构
string是一个安全的二进制字符串(安全:redis中的string是安全字符串,不会被’\0’隔开)
哈希:键值对
双端队列(链表)list:有序(插入有序)
无序集合set:对顺序不关注,里面值都是唯一的
有序集合zset:对顺序关注,里面值是唯一的
五、redis基本使用
打开redis服务端
打开redis服务端,redis.conf是服务端的配置文件
redis-server redis.conf
查看服务端是否已经打开
ps aux|grep redis-server
redis客户端连接服务端
后面不加任何内容,代表登入本机的redis-server
redis-cli
客户端进入后
是这个样子
要进行密码认证才能使用,注意,密码是字符串形式,要加上双引号
auth "yourpassword"
key-value
设置 key 的 value 值
其中key为"hello",value为"world"
(是不能重复的)
set hello world
通过key获得value
get hello
hset
设置一个hash
key=markinfo指向的是一个键值对,filed=age,再对应哈希的值。
可以理解为一个二级字典
(是不能重复的)
hset markinfo age 30
获取值markinfo中age对应的值
hget markinfo age
list
(内容是可以重复的)
往链表liststr中插入“dog”,“cat”,“dog”这三个元素
lpush liststr dog cat dog
从liststr中取数据,begin=0代表起始位置 end=-1代表末尾
lrange liststr 0 -1
pop出liststr的最前面的值
lpop liststr
查看所有的key
keys *
mysql和redis一样,都是严格有序的请求回应的模式
而mongo不一定按 请求的顺序来回应
使用redis的基本步骤
第一步是connect连接
第二步是auth 输入密码,如果没有设置就不需要
第三步是select选择数据库,如果不进行这一步,默认是0号数据库
六、数据结构:string
字符数组,该字符串是动态字符串 raw,字符串长度小于1M 时,加倍扩容;超过 1M 每次只多扩
1M;字符串最大长度为 512M;
注意:redis 字符串是二进制安全字符串;可以存储图片,二进制协议等二进制数据;
基础命令
# 设置 key 的 value 值 SET key val # 获取 key 的 value GET key # 执行原子加一的操作 INCR key # 执行原子加一个整数的操作 INCRBY key increment # 执行原子减一的操作 DECR key # 执行原子减一个整数的操作 DECRBY key decrement # 如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做 SETNX key value # 删除 key val 键值对 DEL key # 设置或者清空key的value(字符串)在offset处的bit值。 SETBIT key offset value # 返回key对应的string在offset处的bit值 GETBIT key offset # 统计字符串被设置为1的bit数. BITCOUNT key
setnx和set的区别:
setnx也就是set not exist,对于存在的key,就不会进行处理
而set不管存在不存在,都是设置,如果存在就会覆盖
存储结构
字符串长度小于等于 20 且能转成整数,则使用 int 存储;
字符串长度小于等于 44,则使用 embstr 存储;
字符串长度大于 44,则使用 raw 存储;
在redis中所有的key都是string
使用不同类型是为了能够节约内存
可以通过object encoding [key] 来查看存储的数据类型
string可以作为位图使用
还可以作为位图使用
设置索引为9的地方为1。 可以看到索引为9的为1,索引为8的为0(因为没有设置)
还可以查看位图中,1的个数
应用
对象存储
SET role:10001 '{["name"]:"mark",["sex"]:"male",["age"]:30}' # 极少修改,对象属性字段很少改变的时候,可以这种写法,否则建议使用哈希 GET role:10001
冒号后面的数字是用来唯一确定那个key的
打开redis客户端,可以看到role是一个文件夹,里面包含了role:10001
因此可以通过,加冒号来不断地定义key,可以理解为文件夹(树状结构)
如
role:10001:recharge # 或者 也可以多级冒号 role:10001:activity:10001
累加器
# 统计阅读数 累计加1 incr reads # 累计加100 incrby reads 100
使用incr key
必须保证为可以将string转化为int,否则会失败
分布式锁
# 加锁 setnx lock 1 # 不存在才能设置 定义加锁行为 占用锁 # 释放锁 del lock if(get(lock)==uuid) del(lock); # 1. 排他功能 2. 加锁行为定义 3. 释放行为定义
uuid表示客户端的唯一标识,nx表示not exist(不存在才能set成功),ex 30表示(expire 30秒后过期)
set lock uuid nx ex 30
互斥锁是一种公平锁,A获取锁,操作临界资源,释放锁后,按照请求顺序获取锁。
而自旋锁是一种非公平锁,通过不断尝试去获取锁(没有严格的顺序,谁请求到了,就获取锁)。
分布式锁是一种非公平锁。
七、数据结构:list
双向链表实现,列表首尾操作(删除和增加)时间复杂度 O(1) ;查找中间元素时间复杂度为
O(n) ;
列表中数据是否压缩的依据:
- 元素长度小于 48,不压缩;
- 元素压缩前后长度差不超过 8,不压缩;
基础命令
# 从队列的左侧入队一个或多个元素 LPUSH key value [value ...] # 从队列的左侧弹出一个元素 LPOP key # 从队列的右侧入队一个或多个元素 RPUSH key value [value ...] # 从队列的右侧弹出一个元素 RPOP key # 返回从队列的 start 和 end 之间的元素 0, 1 2 LRANGE key start end # 从存于 key 的列表里移除前 count 次出现的值为 value 的元素 LREM key count value # 它是 RPOP 的阻塞版本,因为这个命令会在给定list无法弹出任何元素的时候阻塞连接 BRPOP key timeout # 超时时间 + 延时队列
存储结构
/* Minimum ziplist size in bytes for attempting compression. */ #define MIN_COMPRESS_BYTES 48 /* quicklistNode is a 32 byte struct describing a ziplist for a quicklist. * We use bit fields keep the quicklistNode at 32 bytes. * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k). * encoding: 2 bits, RAW=1, LZF=2. * container: 2 bits, NONE=1, ZIPLIST=2. * recompress: 1 bit, bool, true if node is temporary decompressed for usage. * attempted_compress: 1 bit, boolean, used for verifying during testing. * extra: 10 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; unsigned int sz; /* ziplist size in bytes */ unsigned int count : 16; /* count of items in ziplist */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */ unsigned int recompress : 1; /* was this node previous compressed? */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode; typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* total count of all entries in all ziplists */ unsigned long len; /* number of quicklistNodes */ int fill : QL_FILL_BITS; /* fill factor for individual nodes */ unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */ unsigned int bookmark_count: QL_BM_BITS; quicklistBookmark bookmarks[]; } quicklist;
应用
栈(先进后出 FILO)
LPUSH + LPOP # 或者 RPUSH + RPOP
队列(先进先出 FIFO)
LPUSH + RPOP # 或者 RPUSH + LPOP
阻塞队列(blocking queue)
LPUSH + BRPOP # 或者 RPUSH + BLPOP
异步消息队列
操作与队列一样,但是在不同系统间
web发送数据给redis,几个服务器通过阻塞获取数据brpop或者blpop
获取固定窗口记录
就是维护一个固定窗口大小的数据
方法:
1.固定窗口限流
2.截断限流
通过ltrim只保留最近50条数据,来达到限流的效果
八、数据结构:hash
散列表,在很多高级语言当中包含这种数据结构;c++ unordered_map 通过 key 快速索引
value;
基础命令
# 获取 key 对应 hash 中的 field 对应的值 HGET key field # 设置 key 对应 hash 中的 field 对应的值 HSET key field value # 设置多个hash键值对 HMSET key field1 value1 field2 value2 ... fieldn valuen # 获取多个field的值 HMGET key field1 field2 ... fieldn # 给 key 对应 hash 中的 field 对应的值加一个整数值 HINCRBY key field increment # 获取 key 对应的 hash 有多少个键值对 HLEN key # 删除 key 对应的 hash 的键值对,该键为field HDEL key field
存储结构
节点数量大于 512(hash-max-ziplist-entries) 或所有字符串长度大于 64(hash-max-ziplistvalue),则使用 dict 实现;
节点数量小于等于 512 且有一个字符串长度小于 64,则使用 ziplist 实现;
应用
存储对象
hmset hash:10001 name mark age 18 sex male # 与 string 比较 set hash:10001 '{["name"]:"mark",["sex"]:"male",["age"]:18}' # 假设现在修改 mark的年龄为19岁 # hash: hset hash:10001 age 19 # string: get role:10001 # 将得到的字符串调用json解密,取出字段,修改 age 值 # 再调用json加密 set role:10001 '{["name"]:"mark",["sex"]:"male",["age"]:19}'
购物车
# 将用户id作为 key # 商品id作为 field # 商品数量作为 value # 注意:这些物品是按照我们添加顺序来显示的; # 添加商品: hset MyCart:10001 40001 1 lpush MyItem:10001 40001 # 增加数量: hincrby MyCart:10001 40001 1 hincrby MyCart:10001 40001 -1 // 减少数量1 # 显示所有物品数量: hlen MyCart:10001 # 删除商品: hdel MyCart:10001 40001 lrem MyItem:10001 1 40001 # 获取所有物品: lrange MyItem:10001 # 40001 40002 40003 hget MyCart:10001 40001 hget MyCart:10001 40002 hget MyCart:10001 40003
九、数据结构:set
集合;用来存储唯一性字段,不要求有序;
存储不需要有序,操作(交并差集的时候排序)
基础命令
# 添加一个或多个指定的member元素到集合的 key中 SADD key member [member ...] # 计算集合元素个数 SCARD key # SMEMBERS key SMEMBERS key # 返回成员 member 是否是存储的集合 key的成员 SISMEMBER key member # 随机返回key集合中的一个或者多个元素,不删除这些元素 SRANDMEMBER key [count] # 从存储在key的集合中移除并返回一个或多个随机元素 SPOP key [count] # 返回一个集合与给定集合的差集的元素 SDIFF key [key ...] # 返回指定所有的集合的成员的交集 SINTER key [key ...] # 返回给定的多个集合的并集中的所有成员 SUNION key [key ...]
由于set存储时是无序的,因此spop的结果应该是一个随机值
存储结构
元素都为整数且节点数量小于等于 512(set-max-intset-entries),则使用整数数组存储;
元素当中有一个不是整数或者节点数量大于 512,则使用字典存储;
应用
抽奖
# 添加抽奖用户 sadd Award:1 10001 10002 10003 10004 10005 10006 sadd Award:1 10009 # 查看所有抽奖用户 smembers Award:1 # 抽取多名幸运用户 srandmember Award:1 10 # 如果抽取一等奖1名,二等奖2名,三等奖3名,该如何操作?
共同关注
sadd follow:A mark king darren mole vico sadd follow:C mark king darren sinter follow:A follow:C
推荐好友
sadd follow:A mark king darren mole vico sadd follow:C mark king darren # C可能认识的人: sdiff follow:A follow:C
十、数据结构:zset
有序集合;用来实现排行榜;它是一个有序唯一;
基础命令
# 添加到键为key有序集合(sorted set)里面 ZADD key [NX|XX] [CH] [INCR] score member [score member ...] # 从键为key有序集合中删除 member 的键值对 ZREM key member [member ...] # 返回有序集key中,成员member的score值 ZSCORE key member # 为有序集key的成员member的score值加上增量increment ZINCRBY key increment member # 返回key的有序集元素个数 ZCARD key # 返回有序集key中成员member的排名 ZRANK key member # 返回存储在有序集合key中的指定范围的元素 order by id limit 1,100 ZRANGE key start stop [WITHSCORES] # 返回有序集key中,指定区间内的成员(逆序) ZREVRANGE key start stop [WITHSCORES]
byscore根据分数排序,withscores把分数也打印出来
从大到小排序
存储结构
节点数量大于 128或者有一个字符串长度大于64,则使用跳表(skiplist);
节点数量小于等于128(zset-max-ziplist-entries)且所有字符串长度小于等于64(zset-maxziplist-value),则使用 ziplist 存储;
数据少的时候,节省空间; O(n)
数量多的时候,访问性能;O(1) o(logn)
应用
百度热榜
# 点击新闻: zincrby hot:20210601 1 10001 zincrby hot:20210601 1 10002 zincrby hot:20210601 1 10003 zincrby hot:20210601 1 10004 zincrby hot:20210601 1 10005 zincrby hot:20210601 1 10006 zincrby hot:20210601 1 10007 zincrby hot:20210601 1 10008 zincrby hot:20210601 1 10009 zincrby hot:20210601 1 10010 # 获取排行榜: zrevrange hot:20210601 0 9 withscores
延时队列
将消息序列化成一个字符串作为 zset 的 member;这个消息的到期处理时间作为 score,然后用
多个线程轮询 zset 获取到期的任务进行处理
def delay(msg): msg.id = str(uuid.uuid4()) #保证 member 唯一 value = json.dumps(msg) retry_ts = time.time() + 5 # 5s后重试 redis.zadd("delay-queue", retry_ts, value) # 使用连接池 def loop(): while True: values = redis.zrangebyscore("delay-queue", 0, time.time(), start=0, num=1) if not values: time.sleep(1) continue value = values[0] success = redis.zrem("delay-queue", value) if success: msg = json.loads(value) handle_msg(msg) # 缺点:loop 是多线程竞争,两个线程都从zrangebyscore获取到数据,但是zrem一个成功一个失败, # 优化:为了避免多余的操作,可以使用lua脚本原子执行这两个命令 # 解决:漏斗限流
分布式定时器
生产者将定时任务 hash 到不同的 redis 实体中,为每一个 redis 实体分配一个 dispatcher 进程,
用来定时获取 redis 中超时事件并发布到不同的消费者中;
时间窗口限流
系统限定用户的某个行为在指定的时间范围内(动态)只能发生N次;
# 指定用户 user_id 的某个行为 action 在特定时间内 period 只允许发生做多的次数max_count local function is_action_allowed(red, userid, action, period, max_count) local key = tab_concat({"hist", userid, action}, ":") local now = zv.time() red:init_pipeline() -- 记录行为 red:zadd(key, now, now) -- 移除时间窗口之前的行为记录,剩下的都是时间窗口内的记录 red:zremrangebyscore(key, 0, now - period *100) -- 获取时间窗口内的行为数量 red:zcard(key) -- 设置过期时间,避免冷用户持续占用内存 时间窗口的长度+1秒 red:expire(key, period + 1) local res = red:commit_pipeline() return res[3] <= max_count end # 维护一次时间窗口,将窗口外的记录全部清理掉,只保留窗口内的记录; # 缺点:记录了所有时间窗口内的数据,如果这个量很大,不适合做这样的限流;漏斗限流 # 注意:如果用 key + expire 操作也能实现,但是实现的是熔断,维护时间窗口是限流的功能;