快速上手Redis
NoSQL
什么是nosql
- not only sql:解决海量数据和大型集群产生的高并发,高可用,高性能问题数据库解决方案。
- 不是替代关系型数据库,补充关系型数据库性能瓶颈。
为什么需要NoSQL
- High performance - 高并发读写
- Huge Storange - 海量数据的高效存储和访问
- High Scalability && High Availability - 高扩展性和高可用性
NoSQL 的特点
- 易扩展
- 灵活的数据模型
- 大量数据,高性能
- 高可用
数据分类
结构化数据: 通过统一的格式进行存储管理(RDBMS)
非结构化数据:无法通过统一格式进行存储管理(nosql)
半结构化:介于两者之间(xml)
键值(Key-Value)存储数据库
相关产品: Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB
典型应用: 内容缓存,主要用于处理大量数据的高访问负载。
数据模型: 一系列键值对
优势: 快速查询
劣势: 存储的数据缺少结构化
列存储数据库
相关产品: Cassandra, HBase, Riak
典型应用: 分布式的文件系统
数据模型: 以列簇式存储,将同一列数据存在一起
优势: 查找速度快,可扩展性强,更容易进行分布式扩展
劣势: 功能相对局限
文档型数据库
相关产品: CouchDB、MongoDB
典型应用: Web应用(与Key-Value类似,Value是结构化的)
数据模型: 一系列键值对
优势: 数据结构要求不严格
劣势: 查询性能不高,而且缺乏统一的查询语法
图形(Graph)数据库(图)
相关数据库: Neo4J、InfoGrid、Infinite Graph
典型应用: 社交网络
数据模型: 图结构
优势: 利用图结构相关算法。
劣势: 需要对整个图做计算才能得出结果,不容易做分布式的集群方案。
Redis
什么是redis
- c语言编写的键值非关系型数据库。主要用于高速访问的内容缓存,
- redis也可以实现持久化,支持事务操作。
- 常用数据类型: string(字符类型),hash(散列类型),lists(列表类型),set(集合类型),sortedSet(有序集合)。
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。
Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
主数据结构 | key-value存储 |
---|---|
当前最新版本 | 6.0.5 |
SQL | 无 |
在不同节点上存储不同数据 | 分片 |
在多个节点上冗余存储数据 | 多源复制/源副本复制 |
支付并发处理数据 | 是 |
为什么会出现Redis
形如MYSQL,ORACLE这样的关系型数据库是把数据存储在磁盘中,而Redis这样的非关系型数据库会把数据存储在内存中。
首先磁盘的寻址时间为ms,而内存的寻址时间为ns,磁盘比内存在寻址上慢了10W倍;
其次在建表时关系型数据库需要给出schema(数据类型和结构),存储时为行级存储,当数据大量增加时,如果表存在索引,增删改除了维护本身数据还要维护索引,查询速度在并发大的时候会受磁盘的带宽影响速度;而内存的带宽很大,不存在这个问题;
最后就是关系型数据库存在一个数据从磁盘取出放到内存中的IO的成本问题;
所以就诞生了redis这样类型的数据库,除此之外还有memcached,ehcache等等。
Redis特性
速度快
- 首先Redis是将数据储存在内存中的,通常情况下每秒读写次数达到千万级别。
- 其次Redis使用
ANSI C
编写,因为C语言接近操作系统,所以Redis的执行效率很高。 - 最后Redis的处理网络请求部分采用的是单线程,如果想充分利用CPU资源的话,可以多开几个Redis实例来达到目的
为什么单线程还是速度快的原因呢?
- 我们知道Redis的读写都是基于内存的,读写速度都是非常快的,不会出现需要等待很长时间,所以瓶颈并不会出现在请求读写上,所以没必要使用多线程来利用CPU
- 如果使用多线程的话(线程数>CPU数情况下),多线程的创建、销毁、线程切换、线程竞争等开销所需要的时间会比执行读写所损耗的时间还多,那就南辕北辙了,当然这是在数据量小的时候才会这样,如果数据量到达一定量级了,那肯定是多线程比单线程快(线程数<=CPU数情况下)。
类型 | 每秒读写次数 | 随机读写延迟 | 访问带宽 |
---|---|---|---|
内存 | 千万级 | 80ns | 5GB |
SSD盘 | 35000 | 0.1-0.2ms | 100~300MB |
机械盘 | 100左右 | 10ms | 100MB左右 |
持久化
Redis可以通过
RDB
和AOF
两种方式将数据持久化到磁盘上,其中这两种方式的区别如下:
- RDB: 是在 指定的时间间隔内 将 内存中的数据 通过异步生成数据快照并且保存到磁盘中。
- AOF: 相对于
RDB
方式,AOF
方式的持久化更细粒度,把每次数据变化(写、删除操作)都记录AOF文件中,其中AOF又可以配置为always
即实时将记录写到AOF文件中,everysec
每隔一秒将记录写到AOF文件中,no
由系统决定何时将记录写到AOF文件中。
多种数据结构
redis是一种高级的key:value存储系统,其中value支持五种常用数据类型:
- 字符串(strings)
- 字符串列表(lists)
- 字符串集合(sets)
- 有序字符串集合(sorted sets)
- 哈希(hashes)
多语言客户端
- Redis支持多种语言,诸如Ruby,Python, Twisted Python, PHP, Erlang, Tcl, Perl, Lua, Java, Scala, Clojure等。
功能丰富
- Redis支持发布订阅、Lua脚本、事务、Pipeline等功能。
主从复制
- 在Redis中,用户可以通过执行SLAVEOF命令或者SLAVEOF选项,让从服务器去复制主服务器,为高可用和分布式提供了基础。
高可用和集群
- 高可用
- 有了主从复制之后的实现之后,如果想对服务器进行监控
- 那么在
Redis2.6
以后提供了一个Sentinel
(哨兵机制)。 - 顾名思义,哨兵的含义就是监控Redis系统的运行状态,可以启动多个哨兵,去监控Redis数据库的运行状态。其功能有以下两点:
- 监控所有节点数据库是否正常运行
- 主数据库出现故障时,可以通过自动投票机制,在从数据库选举出新的主数据库,实现将从数据库转为主数据库的自动切换。
- 集群
- Redis在3.0版本正式引入了
Redis-Cluster
集群这个特征。 - Redis-Cluster采用无中心架构,每个节点保存完整的数据和整个集群的状态,每个节点都和其他所有节点连接。
- Redis在3.0版本正式引入了
memcached 和 redis的区别
memcached也是key-value为数据结构存储的数据库,早在2003年就发布了,那为什么还会出现redis呢。主要是因为memcached是不存在value类型的概念
Redis中的value类型主要存在五种,分别是 string,hashes,lists,sets ,sorted sets。string类型除了字符类型还可以存储数值类型,还延伸出了位图(bitmaps),更重要的是redis对各种类型提供了相应的API。
举个例子,假设你要操作某个key下对应value的部分元素,则可以通过相应的api直接操作value;而不存在则没有办法做到,只能取value值然后再操作,有可能还要放回去,这样就又增加了一次IO。所以redis的效率是会比memcached来的高。
redis应用场景
缓存(数据查询、短连接、新闻内容、商品内容等等)。(最多使用)
分布式集群架构中的session分离。
聊天室的在线好友列表。
任务队列。(秒杀、抢购、12306等等)
应用排行榜。
网站访问统计。
数据过期处理(可以精确到毫秒)
安装redis
下载
中文官网 http://www.redis.cn
使用
wget
命令下载Redis1
wget http://download.redis.io/releases/redis-5.0.3.tar.gz
redis安装
准备: gcc环境
1
yum -y install gcc-c++
make命令:
1
yum -y install make
解压安装包
1
tar -zxvf redis-5.0.3.tar.gz -C /opt/
切换到目录,编译
1
2
3
4# 进入到Redis的文件夹
cd /opt/redis-5.0.3
# 编译
make这样我们的Redis就编译好了,为了能全局使用Redis的命令,我们还需要执行安装命令,将可执行文件安装到环境变量中。
1
2
3
4
5# (默认安装:/usr/local/bin)
make install
# 手动指定
make install PREFIX=/usr/local/redis复制redis.conf到etc
1
cp redis.conf /etc/
环境变量
1
2
3export REDIS_HOME=/usr/local/redis
export PATH=$PATH:$JAVA_HOME/bin:$REDIS_HOME/bin
source /etc/profile这样Redis的安装就完成了
redis启动
- 我们来启动Redis,其中Redis的启动有三种方式
- 直接启动 直接执行
redis-server
即可启动Redis,这个方式启动实际上就是读取Redis的默认配置启动。 - 动态参数启动 执行
redis-server --port 6380
即可指定端口号启动Redis,更多参数可以参考官方的文档。 - 配置文件启动 执行
redis-server config.conf
即可指定名为config.conf
的配置文件进行启动Redis。
- 直接启动 直接执行
前端启动(服务器前端运行)
服务器: ./redis-server
- 关闭: 强制关闭:ctrl+c
- 正常命令:
./redis-cli shutdown
(推荐)
客户端: ./redis-cli
后端启动
修改配置文件
etc/redis.conf
:1
2bind 192.168.2.101(修改绑定ip)
daemonize yes(修改作为后台守护程序运行)指定配置文件,运行服务器端:
1
./redis-server /etc/redis.conf
关闭服务器端:
kill杀死
./redis-cli shutdown
(推荐)
自带客户端连接:
./redis-cli -h ip地址 -p 端口
图形客户端:
- 需要关闭防火墙或开发端口号
1
2
3C:\Users\fulsun>redis-cli
127.0.0.1:6379> ping
PONG
Redis为什么那么快
- Redis为什么那么多块呢,其实上文中 为什么会出现redis已给出了部分答案。
基于内存
- Redis完全基于内存,绝大部分请求都是纯粹的内存操作,执行效率高;数据存储在内存中,不受到硬盘IO的限制。
单线程模型
Redis 的网络 IO 和键值对读写是由一个线程来完成的;其次单线程也避免了多线程面临的共享资源并发访问控制问题。
Redis的其他功能,如持久化,异步删除,集群数据同步等,还是由额外的线程执行的。
高效的数据结构
- Redis中的每一种数据类型都使用了高效的数据结构,在一定程度上也提高了效率。具体可参考 Redis为何这么快–数据存储角度 这篇文章。
IO多路复用模型
Redis网络框架基于Linux的IO多路复用机制(select/epoll机制)实现一个Redis线程处理多个IO流的效果,提升了其并发性。
该机制允许内核中同时存在多个监听套接字和已连接套接字。Redis不会一直阻塞在某一个特定的监听或连接套接字上,而是内核会一直监听连接请求或数据请求,一旦请求到达就会基于select/epoll提供的事件回调机制交给Redis线程处理。
即一旦检测到有请求就会触发事件,把这些事件放到一个事件队列,Redis对该事件队列进行不断处理,处理的同时调用相应的处理函数进行回调。
若对Epoll模型不了解可参考此文 此文若说不清Epoll原理,那就过来掐死我!
单线程
从客户端接收请求,到执行Redis命令。
Redis处理网络请时候的求单线程可以抽象成这样,通向Redis的路只有一条,且这条路是个单车道,只容的下一辆车同时使用,而我们使用的Redis命令即为这些车辆,当我们执行多个命令的时候,只有等第一个命令执行完成了后面的命令才会执行,否则会一直处于等待状态。
注意点
一次只运行一条命令
拒绝长(慢)命令(keys、flushall、flushdb、slow lua script、mutil/exec、operate、big value)
至于为什么单线程还这么快,这里有个原因,
- Redis客户端的到Redis服务器的网络请求采用了多路I/O复用模型(非阻塞I/O),
- 利用
select
、poll
、epoll
可以同时监听多个流的I/O(客户端到服务器的网络请求)事件的能力, - 在空闲的时候,会把当前线程阻塞掉,当有一个或者多个流有
I/O
事件时,就从阻塞态中唤醒,轮训一遍所有的流并且依次处理就绪的流。这样就算出现有的流的I/O
因为网络原因很慢,也不会影响别的流的I/O
(非阻塞),因为是轮训所有的流的I/O
。 - 这里的“多路”指的是多个网络连接,“复用”指的是复用同一个线程。
Redis通用命令
- Redis一些通用命令,比如删除一个键、计算数据库的大小、设置键的过期时间等,这些命令有很多,这里主要介绍
7
个,完整的命令可以参考官方文档
查看数据的类型
type key
1
2
3
4
5
6127.0.0.1:6379> type name
none
127.0.0.1:6379> type zs
hash
127.0.0.1:6379> type name
string
rename:重命名key
查看key
基本语法:
- 查找所有符合给定模式
pattern
的key
- **返回值: ** 符合给定模式的
key
列表。
1
2
3
4
5KEYS [pattern]
# 查询所有的key
127.0.0.1:6379> keys *
1) "name"- 查找所有符合给定模式
模糊匹配
1
2
3
4
5
6key *
# 特殊匹配
KEYS h?llo 匹配hello、hallo等key。
KEYS h*llo 匹配hllo和haaaaaallo等key。
KEYS h[abe]llo 匹配hallo、hbllo和hello。时间复杂度为**O(N)**,
N
为数据库中Key
的数量。- 这个命令由于时间复杂度为O(N)所以一般生产环境不使用,
Scan命令
语法:
1
2
3
4SCAN cursor [MATCH pattern] [COUNT count]
cursor - 游标。
pattern - 匹配的模式。
count - 指定从数据集里返回多少元素,默认值为 10 。相比于keys命令,scan命令有两个比较明显的优势:
scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。
scan命令提供了limit参数,可以控制每次返回结果的最大条数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16127.0.0.1:6379> keys *
1) "db_number"
2) "key1"
3) "myKey"
127.0.0.1:6379> scan 0 MATCH * COUNT 1
1) "2"
2) 1) "db_number"
127.0.0.1:6379> scan 2 MATCH * COUNT 1
1) "1"
2) 1) "myKey"
127.0.0.1:6379> scan 1 MATCH * COUNT 1
1) "3"
2) 1) "key1"
127.0.0.1:6379> scan 3 MATCH * COUNT 1
1) "0"
2) (empty list or set)我们的Redis中有3个key,我们每次只遍历一个一维数组中的元素。如上所示,SCAN命令的遍历顺序是
0->2->1->3
这个顺序看起来有些奇怪。我们把它转换成二进制就好理解一些了。
00->10->01->11
我们发现每次这个序列是高位加1的。普通二进制的加法,是从右往左相加、进位。而这个序列是从左往右相加、进位的。这一点我们在redis的源码中也得到印证。
在dict.c文件的dictScan函数中对游标进行了如下处理
1
2
3
4v = rev(v);
v++;
v = rev(v);
# 意思是,将游标倒置,加一后,再倒置,也就是我们所说的“高位加1”的操作。
查找当前数据库的key
的数量。
返回值: 返回当前数据库的
key
的数量1
2
3
4
5
6127.0.0.1:6379> DBSIZE
(integer) 3
127.0.0.1:6379> set new_key 5
OK
127.0.0.1:6379> DBSIZE
(integer) 4时间复杂度为**O(1)**,计算的时候不是扫描整个表,因为Redis有个计数器,实时更新Key总数。
检查给定key
是否存在。
返回值: 若key 存在返回
1
,不存在返回0
。1
EXISTS key
时间复杂度为**O(1)**。
1
2
3
4
5
6
7
8127.0.0.1:6379> scan 0
1) "0"
2) 1) "new_key"
2) "age"
3) "name"
4) "address"
127.0.0.1:6379> exists name
(integer) 1
删除指定的一个或者多个key
不存在的
key
会被忽略。**返回值: ** 被删除的
key
的数量.1
DEL key [key ...]
时间复杂度为**O(N)**,
N
为被删除的key
的数量,其中删除单个字符串类型的key
,时间复杂度为O(1)
;删除单个列表、集合、有序集合或哈希表类型的key
,时间复杂度为O(M)
,M
为以上数据结构内的元素数量。1
2
3
4
5
6
7
8
9
10
11127.0.0.1:6379> scan 0
1) "0"
2) 1) "new_key"
2) "age"
3) "name"
4) "address"
127.0.0.1:6379> del name age
(integer) 2
127.0.0.1:6379> keys *
1) "new_key"
2) "address"
给key 设置过期时间
为给定的
key
设置生存时间,当key
过期时,它会被自动删除。这里设置的时间单位是 秒**返回值: ** 设置成功返回
1
,当key
不存在或者设置失败的时候返回0
。时间复杂度为**O(1)**。
1
2
3PEXPIRE key milliseconds 生存时间设置单位为: 毫秒
EXPIRE key seconds 设置key的生存时间(单位: 秒)key在多少秒后会自动删除
PERSIST key 清除生存时间示例
1
2
3127.0.0.1:6379> set name sun
OK
127.0.0.1:6379> expire name 10
查看key的剩余时间
以秒为单位,返回给定
key
的剩余生存时间(TTL,time to live)。**返回值: **
- 当
key
不存在时,返回-2
, - 当
key
存在但是没有设置生存时间时,返回-1
,否则返回key
的剩余生存时间。
- 当
时间复杂度**O(1)**。
1
2
3
4
5
6
7
8
9
10127.0.0.1:6379> set name sun
OK
127.0.0.1:6379> ttl name
(integer) -1
127.0.0.1:6379> expire name 5
(integer) 1
127.0.0.1:6379> ttl name
(integer) 3
127.0.0.1:6379> ttl name
(integer) -2
Redis五种值类型
对于Redis来说,每一种数据结构都有着自己的内部编码,而且是多种实现的,这样Redis会在合适的场景选择合适的内部编码,通过
OBJECT ENCODING [key]
可以参看指定key
的内部编码。**这样做的好处: **
- 改进内部编码,对外的数据结构和命令没有影响,对用户提供黑箱模型。
- 多种内部编码可在不同场景下发挥各自的优势。如:
ziplist
比较节约内存,但是元素比较多的时候,性能会有所下降,此时Redis会将编码自动转换为linkedlist
,性能会有所改善。
string类型
String类型是我们最常用的一种value数据类型,在上文中我们看到string的encoding有三种类型,即对应的三种数据结构实现(int,embstr,raw)。
embstr和raw都是SDS存储(上文已介绍),但embstr只需要一次内存分配空间,而raw需要两次。
int编码字符串对象和embstr编码字符串对象在一定条件下会转化为raw编码字符串对象。
如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17127.0.0.1:6379> set test 1
OK
127.0.0.1:6379> object encoding test
"int"
127.0.0.1:6379> append test 1
(integer) 2
127.0.0.1:6379> object encoding test
"raw"
127.0.0.1:6379> set test1 aa
OK
127.0.0.1:6379> object encoding test1
"embstr"
127.0.0.1:6379> append test1 aa
(integer) 4
127.0.0.1:6379> object encoding test1
"raw"当你存储的是中文时,由于redis是二进制安全的,所以在不同编码下数据的长度是不同的,有兴趣的可以看看这篇文章redis是二进制安全。
命令 | 描述 |
---|---|
set key value | 赋值 |
get key | 取值 |
getset key value | 取值并赋值 |
mset k1 v1 k2 v2 | 赋值多个值 |
mget k1 k2 | 获取多个值 |
del key | 删除 |
incr key | 数值增1 |
incrby key increment | 设置增长数 |
decr key | 减少1 |
decrby key increment | 减少整数值 |
append key value | 追加数值 |
strlen key | 获取长度 |
注意
incr 将指定的 key 的 value 值递增 1,如果这个值不存在,就将这个值设置初始值为 0,并且 +1.
decr 是将指定的 key 的 value 值递减 1,如果这个值不存在,就将这个值设置初始化为 0,并且 -1.
incrby是将指定的 key 的 value 值递增 指定的大学,如果值不存在,则设置为 0,并且加指定的值
decrby将指定的 key 的值 减去 指定的值 如果值不存在,则设置为 0,并且减指定的值
append指定的 key 的 value 后面拼接给定的值,如果给定的 key 不存在,则创建这个 key 并且这个key 的值为后面给定的值
1
2
3
4
5
6127.0.0.1:6379> get num
(nil)
127.0.0.1:6379> incr num
(integer) 1
127.0.0.1:6379> get num
"1"如果这个值不能转成数值,不能增减,则抛出异常
1
2
3
4127.0.0.1:6379> get num
"ab"
127.0.0.1:6379> incr num
(error) ERR value is not an integer or out of range
字符串命令
String数据类型中比较常用的就是作为字符串使用,可以作为缓存存储一些信息,比如登录用户的一些信息等。如果对命令不太熟悉的前提下我们在Linux系统中可以在连接redis客户端的前提下使用
help @String
来获取各个命令的作用和语法或者在redis命令中心查看。作为字符串常用的命令包括以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29## ex(秒) 和 px(毫秒) 表示key的过期时间 nx 表示不存在才操作 xx表示存在才操作
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
summary: Set the string value of a key
since: 1.0.0
## set 多个 key value
MSET key value [key value ...]
summary: Set multiple keys to multiple values
since: 1.0.1
## 在原value后追加字符串
APPEND key value
summary: Append a value to a key
since: 2.0.0
## 在value的某个偏移量上重写新的value值
SETRANGE key offset value
summary: Overwrite part of a string at key starting at the specified offset
since: 2.2.0
## 获取value的值长度
STRLEN key
summary: Get the length of the value stored in a key
since: 2.2.0
## 获取旧值并设置新值
GETSET key value
summary: Set the string value of a key and return its old value
since: 1.0.0
字符串数值命令
当作为数值使用时,其操作都是原子性,故可以作为规避并发的一种手段,其应用场景包括 秒杀、点赞、收藏,抢购等。
作为数值常用的命令如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24## value + 1
INCR key
summary: Increment the integer value of a key by one
since: 1.0.0
## value - 1
DECR key
summary: Decrement the integer value of a key by one
since: 1.0.0
## value + increment
INCRBY key increment
summary: Increment the integer value of a key by the given amount
since: 1.0.0
## value - increment
DECRBY key decrement
summary: Decrement the integer value of a key by the given number
since: 1.0.0
## value + increment (浮点型)
INCRBYFLOAT key increment
summary: Increment the float value of a key by the given amount
since: 2.6.0
bitmap
bitmap不是实际的数据类型,而是在String类型上定义的一组面向位的操作。根据官方给出的文档提示,位图最大的优势就是在存储时能节省大量空间。其常用的应用场景包括统计活跃用户或用户登录天数等。
1
2
3
4
5
6
7
8127.0.0.1:6379> set hello big
OK
127.0.0.1:6379> getbit hello 0
(integer) 0
127.0.0.1:6379> getbit hello 1
(integer) 1
127.0.0.1:6379> getbit hello 2
(integer) 1
命令
1 | ## 统计key从start到end的被设置的为1的bit数 start end 是字节索引 |
简单演示
这里对bitmap进行简单演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27redis> BITCOUNT bits
(integer) 0
redis> SETBIT bits 0 1 # 0001
(integer) 0
redis> BITCOUNT bits
(integer) 1
redis> SETBIT bits 3 1 # 1001
(integer) 0
redis> BITCOUNT bits
(integer) 2
127.0.0.1:6379> setbit bit 1 1
(integer) 0
127.0.0.1:6379> setbit bit 7 1
(integer) 0
# 分别在key为bit的二进制索引位置1和二进制位置7设置为1,这样实际上就是1000 0010 根据ASCII编码该值就是A。
127.0.0.1:6379> get bit
"A"
# 表示有2个位被设置为1
127.0.0.1:6379> bitcount bit
(integer) 2当添加start 和 end参数时 该值表示字节索引,从右到左
1
2
3
4
5
6
7
8
9
10
11127.0.0.1:6379> setbit bit 16 1
(integer) 0
127.0.0.1:6379> get bit
"A\x00\x80"
# 在第二进制索引位置16设置为1 即 10000000 01000001
# 当统计第1个字节到被设置为1的数量时则是2个
127.0.0.1:6379> bitcount bit 0 1
(integer) 2
# 当不添加则表示所有字节上述实例就是3个。
127.0.0.1:6379> bitcount bit
(integer) 3首先在二进制索引5设置为1 即 0001 0000 ,
- 然后分别获取第一个为0和第一个为1的索引位置;
- 然后再二进制索引8设置1 即 10010000 ,
- 当设置start 和 end时 该值表示字节索引
- 结果即为 5 和 8。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# 1 0000
127.0.0.1:6379> setbit test 5 1
(integer) 0
# 获取第一个为1的索引位置;
127.0.0.1:6379> bitpos test 1
(integer) 5
# 获取第一个为0的索引位置;这里显示的都是0)
127.0.0.1:6379> bitpos test 0
(integer) 0
127.0.0.1:6379> setbit test 8 1
(integer) 0
# 开始索引分别从第二个字节开始
127.0.0.1:6379> bitpos test 1 1
(integer) 8
# 开始索引分别从第一个字节 10010000
127.0.0.1:6379> bitpos test 1 0
(integer) 5
应用场景
用途我们在上面介绍时分别都有提及到,不过作为字符串和数值的用途比较常见,此处不做撰述。我们来仔细看看作为bitmap时的应用场景如何使用。
首先就是统计用户登录天数。假设你用关系型数据库去处理,每个用户登录一天你有相关的登录记录表去录,如果你有1千万的活跃用户每天都在登录你的系统那么你每个人都去记录一条数据,一年365天光数据存储都占用了非常多的资源。如果是使用bitmap你可以这样做,登录人的id作为key,每一位表示一天,即如下表。(1表示登录过 ,0表示未登录)
1
2
3
4
5
6127.0.0.1:6379> setbit user01 1 1
127.0.0.1:6379> setbit user01 2 1
127.0.0.1:6379> setbit user01 16 1
# 出用户登录天数
127.0.0.1:6379> bitcount user01
(integer) 3
用户\天数 | 1 | 2 | 3 | 4 | 5 | … | 364 | 365 |
---|---|---|---|---|---|---|---|---|
userId1 | 0 | 1 | 1 | 0 | 1 | … | 0 | 1 |
userId2 | 1 | 1 | 0 | 1 | 1 | … | 1 | 0 |
… | … | … | … | … | … | … | … | … |
userId99 | 1 | 1 | 1 | 1 | 0 | … | 1 | 1 |
当用户登录使用
setbit userId offset(哪一天)1
就可以记录该用户当天登录,然后使用bitcount
即可以得出用户登录天数,或者你需要获取某个时间周期内的登录天数只要在bitcount
加上start
和end
即可,最最重要的是它占用的空间非常少比起关系型数据库少太多。其次就是活跃用户统计,比如你要统计日🔥用户,那么你也可以使用bitmap来做,思路和上面差不多,只不过现在是把日期作为key,用户的id映射为位,即如下表。(1表示活跃,0表示不活跃)
日期\用户 | 1 | 2 | 3 | 4 | 5 | … | 364 | 365 |
---|---|---|---|---|---|---|---|---|
20200818 | 0 | 1 | 1 | 0 | 1 | … | 0 | 1 |
20200819 | 1 | 1 | 0 | 1 | 0 | … | 1 | 1 |
- 当用户活跃时,使用
setbit 20200818 offset(用户id映射的位)1
即表示用户活跃,然后使用bitcount
即可以得出当天的活跃用户;如果你需要对某个时间周期内进行统计且需要去重则可使用bitop or destkey 时间key1 时间key...
然后再对destKey
进行bitcount
即可。
假设每个用户存储需要20B空间, 那么1亿个用户所占用的空间就是 20 * 100000000 = 20000000000B ≈ 18G,看起来也是个不小的内存消耗;如果你对精确度不是很高的要求那么可以考虑Redis提供的Hypeloglog,可以参考下 Redis HyperLogLog介绍及应用。
hash类型
散列类型,支持扩展key,提供字段属性,字段和字段值之间实现映射。
使用场景:存储对象信息(用户,商品信息等)
字段值只支持string类型。
命令 描述 HSET key field value 设置值,,0代表更新,1代表插入 HMSET key field value [field value …] 一次设置多个值 HSETNX key field value 设置字段的值,如果字段已存在,该操作无效果。 hget key field 获取值 hmget key field [field …] 获取多个值 hgetall key 获取所有的值 hdel key field [field …] 删除一个属性或多个属性 del key 删除整个 key HINCRBY key field increment 增加数字,不存在给默认值0 hexists key field 返回hash里面field是否存在 hlen key 获取某个 key 存在的属性个数 hkeys key 获取所有属性名称 hvals key 获取属性所有的值 1
2
3
4
5
6
7
8
9
10
11
12
13192.168.2.101:6379> HSET user:001 name zs
(integer) 1
192.168.2.101:6379> HSET user:001 name ls
(integer) 0
192.168.2.101:6379> HGET user:001 name
"ls"
192.168.2.101:6379> HMSET user:001 name zs age 12
OK
192.168.2.101:6379> HMGET user:001 name age
1) "zs"
2) "12"
有该key则不赋值,没有则赋值
192.168.2.101:6379> HSETNX user:001 name ww- Hash数据类型就是 key-value 键值对,对应的encoding可以是ziplist或者hashtable。 Hash对象只有同时满足下面两个条件时,才会使用ziplist(压缩列表):
- 哈希中元素数量小于512个;
- 哈希中所有键值对的键和值字符串长度都小于64字节。这样会非常节约内存。
- 其应用场景包括点赞,收藏等。
- 对其数据结构有兴趣的仍然可以了解Redis为何这么快–数据存储角度的Hash篇。
- Hash数据类型就是 key-value 键值对,对应的encoding可以是ziplist或者hashtable。 Hash对象只有同时满足下面两个条件时,才会使用ziplist(压缩列表):
常用的命令如下:
1 | ## 设置多个键值对 |
应用场景
Hash的数据类型可以用作点赞,收藏,阅读数等。比如你有一个帖子,你需要为它统计点赞,收藏,阅读。如果你使用string类型的话 你肯定是要设置3个key分别对应点赞,收藏,阅读,这样在无形中也浪费了存储空间;但是如果你使用hash的话 一个key就搞定,使用hset 帖子id 点赞数 0 收藏数 0 阅读 0
,当每增加一个变化时 使用HINCRBY key field increment
对field进行数值计算就可以解决。
其次就是当你缓存一些数据时,比如说用户如果你使用string缓存,那么必然是个json对象,当你只需要某个值时你需要把整个对象取出来然后处理,但hash就可以直接取出对应的值。
list类型
在 redis 中。list 是按照插入排序的一个字符串的链表,和数据结构中链表是一样的,
可以再头部(左侧)或者尾部(右侧)增加或者删除元素。
按照插入顺序排序
- ArrayList 使用数组方式
- LinkdList 使用双向链接方式
- 双向链表增加数据
- 双向链表删除数据
使用场景:用于商品评论
常用命令
命令 | 描述 |
---|---|
lpush key value [value …] | 左端添加,如果不存在,则创建一个 list,如果存在,则添加进 key 的 list |
LPUSHX key value | 只有当 key 已经存在并且存着一个 list 的时候,在这个 key 下面的 list 的头部插入 value。 与 LPUSH 相反,当 key 不存在的时候不会进行任何操作。 |
rpush key value | 右端添加 |
rpushx key value | 将一个值插入到已存在的列表w尾部,如果不存在,则不会插入,返回 (integer) 0 |
lrange key [start] [end] | 查看列表,index:从0开始,-1代表最后一位 |
lpop key | 左边弹出, 如果存在,返回头部的第一个元素,如果不存在,返回 nil.弹出以后就没有这个元素 |
rpop key | 右边弹出 |
llen key | 获取列表元素个数 |
lrem key count value | 从列表中删除元素 |
LINDEX key index | 查看指定索引的值 |
ltrim key start stop | 修剪片段,list 就会只包含指定范围的指定元素,start 和 stop 都是由0开始计数的 |
LINSERT key BEFORE/AFTER pivot value | 把 value 插入存于 key 的列表中在基准值 pivot 的前面或后面。当 key 不存在时,这个list会被看作是空list,任何操作都不会发生.当 key 存在,但保存的不是一个list的时候,会返回error。 |
RPOPLPUSH source destination timeout | 原子性地返回并移除存储在 source 的列表的最后一个元素(列表尾部元素), 并把该元素放入存储在 destination 的列表的第一个元素位置(列表头部) |
BRPOPLPUSH source destination timeout | BRPOPLPUSH 是 RPOPLPUSH 的阻塞版本。当 source 包含元素的时候,这个命令表现得跟 RPOPLPUSH 一模一样。 当 source 是空的时候,Redis将会阻塞这个连接,直到另一个客户端 push 元素进入或者达到 timeout 时限。 timeout 为 0 能用于无限期阻塞客户端。 |
lrem 从存于 key 的列表里移除前 count 次出现的值为 value 的元素。 这个 count 参数通过下面几种方式影响这个操作:
count > 0: 从头往尾移除值为 value 的元素。
count < 0: 从尾往头移除值为 value 的元素。
count = 0: 移除所有值为 value 的元素。
比如, LREM list -2 “hello” 会从存于 list 的列表里移除最后两个出现的 “hello”。
需要注意的是,如果list里没有存在key就会被当作空list处理,所以当 key 不存在的时候,这个命令会返回 0。
示例
1 | 127.0.0.1:6379> lpush mylist a b c |
ListAPI
List数据类型就是有序(插入顺序)元素的序列,其对应的encoding为上文表格中的ziplist和linkelist。Redis中的列表支持两端插入和弹出,并且可指定位置获取元素,可以充当数组,队列,栈等。因此,其应用场景包括微博的时间轴列表,阻塞队列等。
这里不对底层数据结构进行分析,若想了解zipList和linkedList可参考 Redis为何这么快–数据存储角度的List篇。
- 常用的命令如下:
1 | ## 获取列表index的值(从左开始) |
- 以上罗列的是从list的左侧开始,同样的redis也提供了从list的右侧开始。redis同样提供了阻塞式的一些命令。如下所示:
1 | ## 阻塞获取列表元素并删除(左侧) 可指定多个列表 同时可设置timeout时间(单位秒) |
这里要注意的是当有多个连接对同一个list进行阻塞监听时,redis的处理方法是维护了一个阻塞队列,提供先阻塞先服务,当多个阻塞同时满足唤醒条件时,先阻塞的优先唤醒。
应用场景
上面我们说List可以充当栈,队列。栈的特征是先进后出,那么在使用Redis的同一个方向命令(
lpush lpop
)时就可以实现栈的特点;队列的特征是先进先出,那么同样的在使用Redis的反向命令(lpush rpop
或rpush lpop
)时就可以实现队列的特点。微博的时间轴列表也比较简单,每次新增一个微博时只需要
lpush
进去即可,通过lrange
来获取最新的微博消息。阻塞队列的实现就需要使用
blpop
或brpop
,首先一定要先调用blpop
监听,然后再另外一个客户端进行lpush
操作,如果先lpush
再blpop
那么先lpush
的数据就不会被监听到。
set
- 无序,不重复
- Set是String的无序去重排列,对应的encoding是intset(整数集合)或hashtable。intset(整数集合)当一个集合只含有整数,并且元素不多时会使用intset(整数集合)作为Set集合对象的底层实现。其应用场景包括共同关注,随机事件等。
常用的命令
1 | # 集合内操作 |
应用场景
- Set的应用场景包括随机事件,公共好友。这其实就对应了Set的集合内命令和集合间命令。集合内命令的
spop key
和srandmember key count
就可以应对随机事件,比如抽奖系统,key为奖品,member为用户id,如果希望中奖的人不会再中就可以使用spop
,如果希望重复中奖就可以使用srandmember
;集合间命令获取交集sinter
就可以应对共同好友,共同关注,key为用户,member分别为用户的关注。
命令 | 描述 |
---|---|
SADD key member [member …] | 添加一个或多个指定的member元素到集合的 key中 |
spop key | 删除元素 |
SMEMBERS key | 查看所有元素 |
SMEMBER key member | 判断是否存在,是集合key的成员,则返回1,集合key不存在,则返回0 |
scard key | 长度 |
SUNION k1 k2 | 并集 |
SINTER k1 k2 | 交集 |
SDIFF k1 k2 | 差集 |
1 | 127.0.0.1:6379> sadd set1 a |
sortedset
又名zset,唯一且可排序。
为每个元素设置分数,根据分数实现排序。
应用场景: 排行榜
Sorted Set是在Set上添加一个分数(score)维度的集合,通过分数来为集合中的元素进行排序。相比于Set来说Sorted Set可理解为一个有序无重复的序列,其encoding为ziplist或skiplist。当一个有序集合的元素数量比较多或者成员是比较长的字符串时,Redis就使用skiplist(跳跃表)作为ZSet对象的底层实现。其应用场景主要包括排行榜等。
常用命令如下
1 | ## 添加集合元素 NX 不更新 只添加 XX只更新 不添加 CH 返回值为修改总数(默认是新增总数) INCR 分值添加 |
应用场景
- Sorted Set最常见的应用场景便是排行榜,使用
zadd key score member
就可以实现自动排序,默认按照从小到大排序,如果你希望倒序那就使用zrevrangebyscore
根据分数倒序即可,想获取某一个元素的排名使用zrank
,但要记住这仍是正序的,因为sorted set物理内存不随命令变化,想要倒序的排名使用zrevrank
。
命令 | 描述 |
---|---|
zadd key score member [score member … ] | 添加元素 |
ZSCORE key member | 查看排序的分数 |
ZREM key score member [score member … ] | 删除元素 |
ZRANGE key start stop [WITHSCORES] | start 和stop 都是基于零的索引,顺序输出[start,stop]包含的区间 |
ZREVRANGE key start stop [WITHSCORES] | 倒序输出 |
ZRANK key member | 返回有序集key中成员member的排名,其中有序集成员按score值递增(从小到大)顺序排列。排名以0为底,也就是说,score值最小的成员排名为0 |
ZREVRANK key member | |
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] | 如果M是常量(比如,用limit总是请求前10个元素),你可以认为是O(log(N))。返回key的有序集合中的分数在min和max之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的。min和max可以是-inf和+inf,这样一来,你就可以在不知道有序集的最低和最高score值的情况下,使用ZRANGEBYSCORE这类命令。 |
ZINCRBY key increment member | 增加某个成员分数 |
zcard key | 查看元素个数 |
ZCOUNT key min max | 统计范围内的元素个数 |
ZREMRANGEBYRANK key start stop | 根据排名删除范围内元素 |
ZREMRANGEBYSCORE key start stop | 根据分数删除范围内元素 |
1 | # 添加元素 |
RedisObject
redisObject是redis中封装value对象的数据结构。任何一个value都会被包装成一个redisObject。redisObject能指定value的类型,编码格式,内存回收,数据指针等。
这样设计的好处是在5种常用类型设置多种不同的的数据结构实现,优化对象在不同场景下的效率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16typedef struct redisObject {
// 刚刚好32 bits
// 对象的类型,字符串/列表/集合/哈希表
unsigned type:4;
// 未使用的两个位
unsigned notused:2; /* Not used */
// 编码的方式,Redis 为了节省空间,提供多种方式来保存一个数据
// 譬如: “123456789” 会被存储为整数123456789
unsigned encoding:4;
// 当内存紧张,淘汰数据的时候用到
unsigned lru:22; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 数据指针
void *ptr;
} robj这里介绍几个比较关键的属性:
type: 标记了value对象的数据类型,使用
type
命令来获取。所有数据类型如下所示:1
2
3
4
5
6/* Object types */
encoding: 标记了value对象的编码,也就是使用了什么数据结构,使用
object encoding
命令来获取。编码如下所示:1
2
3
4
5
6
7
8
9
10
11/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */redis为优化内存,对数据类型提供了多种底层实现方式,type和encoding对应关系如下表格所示:
类型(type属性) 编码(encoding属性) 注释 REDIS_STRING REDIS_ENCODING_INT 整数值实现的字符串 REDIS_STRING REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串实现的字符串 REDIS_STRING REDIS_ENCODING_RAW 简单动态字符串实现的字符串 REDIS_LIST REDIS_ENCODING_ZIPLIST 压缩列表实现的列表 REDIS_LIST REDIS_ENCODING_LINKEDLIST 双向链表实现的列表 REDIS_HASH REDIS_ENCODING_ZIPLIST 压缩列表实现的哈希表 REDIS_HASH REDIS_ENCODING_HT 字典实现的哈希表 REDIS_SET REDIS_ENCODING_INTSET 整数集合实现的集合 REDIS_SET REDIS_ENCODING_HT 字典实现的集合 REDIS_ZSET REDIS_ENCODING_ZIPLIST 压缩列表实现的有序集合 REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳跃表+字典实现的有序集合 lru: redis对数据集占用内存的大小由周期性的计算,当超出限制时,会淘汰超时的数据。即淘汰的标准为: oversize & overtime。
举个栗子,当set hello world时会有以下数据模型:
- dictEntry: Redis给每个key-value键值对分配一个dictEntry,里面有着key和val的指针,next指向下一个dictEntry形成链表,这个指针可以将多个哈希值相同的键值对链接在一起,由此来解决哈希冲突问题(链地址法)。
- **sds: **键key “hello” 是以SDS(简单动态字符串)存储。
- redisObject: 值val是存储在redisObject。
简单动态字符串: 长度动态可变,可类比为Java中的ArrayLIst。Redis中不仅Key是以SDS形式存储,String类型value对象也有以SDS存储。
1
2
3
4
5struct sdshdr {
int len; // buf数组中已经使用的字节的数量,也就是SDS字符串长度
int free; // buf数组中未使用的字节的数量
char buf[]; // 字节数组,字符串就保存在这里面
};- **常数复杂度获取字符串长度: **len字段存储字符串长度
- **预空间分配: **
- SDS长度(len的值)小于1MB,那么程序将分配和len属性同样大小的未使用空间,这时free和len属性值相同。
- SDS长度(len的值)大于等于1MB,程序会分配1MB的未使用空间。
- 惰性释放空间: 当执行sdstrim(截取字符串)之后,SDS不会立马释放多出来的空间,如果下次再进行拼接字符串操作,且拼接的没有刚才释放的空间大,则那些未使用的空间就会排上用场。通过惰性释放空间避免了特定情况下操作字符串的内存重新分配操作。
- 杜绝缓冲区溢出: 使用C字符串的操作时,如果字符串长度增加(如strcat操作)而忘记重新分配内存,很容易造成缓冲区的溢出;而SDS由于记录了长度,相应的操作在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。