redis基本操作、数据结构以及应用

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: redis基本操作、数据结构以及应用

如何学习redis?

  • 了解redis是什么
  • redis如何使用,如何操作redis中的数据结构
  • 典型应用和操作
  • 阅读redis源码

Redis概述

Redis 是Remote Dictionary Service 的简称;也是远程字典服务;

Redis 是内存数据库,KV 数据库,数据结构数据库;

Redis 应用非常广泛,如Twitter、暴雪娱乐、Github、Stack Overflow、腾讯、阿里巴巴、京

东、华为、新浪微博等,很多中小型公司也在使用;

Redis命令查看:redis命令手册

Redis应用

  • 记录朋友圈点赞数、评论数和点击数(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

Redis是内存数据库,kv数据库,数据结构数据库

有string,hash,list,set,zset

server向redis发送命令,来操作redis中的数据结构,redis应答操作结果

redis默认有16个数据库

redis只能单线程使用,同时只能使用一个数据库,用字典的方式组织起来,而mysql使用b+树组织的

redis中的数据结构

string 是一个安全的二进制字符串(以长度作为分隔符,不以特殊字符作为分隔符);

双端队列 (链表)list:有序(插入有序);

散列表 hash:对顺序不关注,field 是唯一的; 注意hash不能嵌套

无序集合 set:对顺序不关注,里面的值都是唯一的;

有序集合 zset:对顺序是关注的,里面的值是唯一的;根据 member 来确定唯一;根据 score 来确定有序;

Redis存储结构

Redis中的value编码

redis是一个内存数据库,设计时到底是要运行速度快,还是要存储效率高呢?

  1. 数据量少的时候,存储效率高为主;
  2. 数据量多的时候,运行速度快;

因为如上的考虑,所以redis的value中就会涉及到非常多的编码方式

Redis基本使用

使用redis的基本步骤

  1. connect连接
  2. auth 输入密码,如果没有设置就不需要
  3. select选择数据库,如果不进行这一步,默认是0号数据库

打开redis服务端,redis.conf是服务端的配置文件

redis-server redis.conf

redis客户端连接服务器:后面不加任何内容,代表登入本机的redis-server

redis-cli

客户端进入后是这个样子

要进行密码认证才能使用,注意,密码是字符串形式,要加上双引号

auth "yourpassword"

注意:

可以通过redis命令手册去查阅redis命令的返回值,redis的返回值非常重要,一定要非常关注,因为redis命令的返回值直接决定了业务逻辑的方向

Redis数据结构

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 存储;

使用不同类型是为了能够节约内存

可以通过object encoding [key] 来查看存储的数据类型

应用
对象存储
SET role:10001 '{["name"]:"mark",["sex"]:"male",
["age"]:30}'
SET role:10002 '{["name"]:"darren",["sex"]:"male",
["age"]:30}'
# 极少修改,对象属性字段很少改变的时候
GET role:10001
# key 如何来设置
# 1. 有意义的字段 role 有多行
# 2. role:10001 redis 客户端 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获取锁,操作临界资源,释放锁后,按照请求顺序获取锁。

而自旋锁是一种非公平锁,通过不断尝试去获取锁(没有严格的顺序,谁请求到了,就获取锁)。

分布式锁是一种非公平锁。

位运算
# 猜测一下 string 是用的 int 类型 还是 string 类型
# 月签到功能 10001 用户id 202106 2021年6月份的签到 6月份的第1天
setbit sign:10001:202106 1 1
# 计算 2021年6月份 的签到情况
bitcount sign:10001:202106
# 获取 2021年6月份 第二天的签到情况 1 已签到 0 没有签到
getbit sign:10001:202106 2

list

双向链表实现,列表首尾操作(删除和增加)时间复杂度 O(1) ;查找中间元素时间复杂度为O(n) ;

列表中数据是否压缩的依据:

  1. 元素长度小于 48,不压缩;
  2. 元素压缩前后长度差不超过 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 # 超时时间 + 延时队列
存储结构

应用
LPUSH + LPOP
# 或者
RPUSH + RPOP
队列
LPUSH + RPOP
# 或者
RPUSH + LPOP
阻塞队列
LPUSH + BRPOP
# 或者
RPUSH + BLPOP
异步消息队列

操作与队列一样,但是在不同系统间

web发送数据给redis,几个服务器通过阻塞获取数据brpop或者blpop

获取固定窗口记录

就是维护一个固定窗口大小的数据

方法:

1.固定窗口限流

2.截断限流

Redis Ltrim 命令

通过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 操作也能实现,但是实现的是熔断,维护时间窗口是限流的功能;


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
3天前
|
存储 NoSQL 算法
Redis入门到通关之Redis数据结构-Hash篇
Redis入门到通关之Redis数据结构-Hash篇
12 1
|
3天前
|
存储 NoSQL Redis
Redis入门到通关之Redis数据结构-List篇
Redis入门到通关之Redis数据结构-List篇
|
3天前
|
存储 NoSQL 安全
Redis入门到通关之Redis数据结构-String篇
Redis入门到通关之Redis数据结构-String篇
|
3天前
|
存储 NoSQL Redis
Redis入门到通关之数据结构解析-SkipList
Redis入门到通关之数据结构解析-SkipList
|
3天前
|
存储 NoSQL 安全
Redis入门到通关之数据结构解析-动态字符串SDS
Redis入门到通关之数据结构解析-动态字符串SDS
11 0
|
20天前
|
消息中间件 存储 搜索推荐
深入理解栈和队列(二):队列
深入理解栈和队列(二):队列
34 0
|
1月前
【栈】数据结构栈的实现
【栈】数据结构栈的实现
|
1月前
|
存储
数据结构--栈和队列
数据结构--栈和队列
|
1月前
|
存储 算法 数据处理
数据结构从入门到精通——栈
栈,作为一种后进先出(LIFO)的数据结构,在计算机科学中扮演着重要的角色。它的特性使得它在处理函数调用、括号匹配、表达式求值等问题时具有得天独厚的优势。然而,如果我们跳出传统思维的束缚,会发现栈的用途远不止于此。
58 0
|
1月前
|
C语言
数据结构之栈详解(C语言手撕)
数据结构之栈详解(C语言手撕)
37 1