1 Nosql概述
1.1 为什么要用Nosql
1.1.1 单机MySQL的年代!
90年代,一个基本的网站访问量一般不会太大,单个数据库完全足够!那个时候,更多的去使用静态网页 ~ 服务器根本没有太大的压力!这种情况下:整个网站的瓶颈是什么?
- 数据量如果太大、一个机器放不下了。
- 数据的索引(B+ Tree),一个机器内存也放不下。
- 访问量(读写混合),一个服务器承受不了。
1.1.2 Memcached(缓存)+ MySQL + 垂直拆分(读写分离)
网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据的压力,我们可以使用缓存来保证效率!
发展过程: 优化数据结构和索引 -> 文件缓存(IO)-> Memcached。
1.1.3 分库分表 + 水平拆分 + MySQL集群
- 早些年 MyISAM: 表锁,十分影响效率!高并发下就会出现严重的锁问题。
- 使用 Innodb:行锁。
- 慢慢的就开始使用分库分表来解决写的压力! MySQL 在哪个年代推出 了表分区!这个并没有多少公司
使用。 - MySQL 的 集群,很好满足哪个年代的所有需求。
1.1.4 如今的年代
- MySQL 等关系型数据库就不够用了!数据量很多,变化很快~!
- MySQL 有的使用它来存储一些比较大的文件,博客,图片!数据库表很大,效率就低了!如果有一种数
据库来专门处理这种数据。MySQL压力就变得十分小。
目前一个基本的互联网项目:
用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!这时候我们就需要使用NoSQL数据库的,Nosql 可以很好的处理以上的情况!
1.2 什么是NoSQL
NoSQL = Not Only SQL (不仅仅是SQL)和关系型数据库相对,泛指非关系型数据库。随着 web2. 0互联网的诞生!传统的关系型数据库很难对付 web2.0 时代!NoSQL 在当今大数据环境下发展的十分迅速,Redis 是发展最快的,而且是我们当下必须要掌握的一个技术!
1.2.1 NoSQL 特点
- 方便扩展(数据之间没有关系,很好扩展)。
- 大数据量高性能(Redis 一秒写8万次,读取11万,NoSQL的缓存记录级,是一种细粒度的缓存,性
能会比较高)。 - 数据类型是多样型的。
- 传统 RDBMS(关系型数据库) 和 NoSQL
传统的 RDBMS
- 结构化组织
- SQL
- 数据和关系都存在单独的表中
- 操作操作,数据定义语言
- 严格的一致性
- 基础的事务
- .....
Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性
- CAP定理和BASE (异地多活)
- 高性能,高可用,高可扩
- ....
1.2.2 3V + 3高
大数据时代的3V:主要是描述问题的
- 海量Volume
- 多样Variety
- 实时Velocity
大数据时代的3高:主要是对程序的要求
- 高并发
- 高可扩
- 高性能
1.3 NoSQL的四大分类
KV 键值对:
- Redis
- Tair
- MemCache
文档型数据库:
- MongoDB
- MongoDB 是一个基于分布式文件存储的数据库,C++ 编写,主要用来处理大量的文档!
- MongoDB 是一个介于关系型数据库和非关系型数据中中间的产品!MongoDB 是非关系型数
据库中功能最丰富,最像关系型数据库的!
- ConthDB
列存储数据库:
- HBase
- 分布式文件系统
图关系数据库:
- 存放图关系,比如社交网络
- Neo4j
- InfoGrid
四者对比表
分类 | 举例 | 数据模型 | 优点 | 缺点 | 典型应用场景 |
---|---|---|---|---|---|
键值(Key-Value)存储数据库 | Redis | Key指向Value的键值对,通常用hash表来实现 | 查找速度快 | 数据无结构化(通常只被当作字符串或者二进制数据) | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等 |
文档型数据库 | MongoDB | Key-Value对应的键值对,Value为结构化数据 | 数据结构要求不严格,表结构可变(不需要像关系型数据库一样需预先定义表结构) | 查询性能不高,而且缺乏统一的查询语法 | Web应用 |
列存储数据库 | Hbase | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 | 分布式的文件系统 |
图关系数据库 | Neo4J | 图结构 | 利用图结构相关算法(如最短路径寻址,N度关系查找等) | 很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群方案 | 社交网络,推荐系统等 |
2 Redis入门
2.1 概述
- Redis 是什么
- Redis(Remote Dictionary Server ),即远程字典服务 。是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
- Redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了
master-slave(主从)同步。
- Redis 能干嘛
- 内存存储、持久化,内存中是断电即失、所以说持久化很重要(rdb、aof)
- 效率高,可以用于高速缓存
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(浏览量!)
- 特性
- 多样的数据类型
- 持久化
- 集群
- 事务
- Reds 官网地址
官网:https://redis.io/
中文网:http://www.redis.cn/
2.2 Linux 安装
1.下载安装包 redis-6.2.4.tar.gz
2.解压Redis的安装包到 /opt 下
3.进入解压后的文件,可以看到我们redis的配置文件
4.基本的环境安装
# 安装 c 编译环境
yum install gcc-c++
# 开始编译
make
# 验证安装
make install
5.redis的默认安装路径 /usr/local/bin
6.将redis配置文件。复制到我们当前目录下
7.redis默认不是后台启动的,修改配置文件
8.启动Redis服务
9.使用redis-cli 进行连接测试
10.查看redis的进程是否开启
11.如何关闭Redis服务呢 shutdown
2.3 测试性能
使用官方自带的性能测试工具 redis-benchmark
我们来简单测试下:
# 测试:100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
2.4 基础知识
redis默认有16个数据库。
默认使用的是第0个。
可以使用 select 进行切换数据库。
127.0.0.1:6379> select 3 # 切换数据库
OK
127.0.0.1:6379[3]> DBSIZE # 查看 DB 大小
(integer) 0
127.0.0.1:6379[3]> keys * # 查看当前数据库所以 key
1) "key"
127.0.0.1:6379[3]> FLUSHDB # 清空当前数据库
OK
127.0.0.1:6379[3]> keys *
(empty array)
127.0.0.1:6379[3]> FLUSHALL # 清空所有数据库
OK
127.0.0.1:6379[3]> SELECT 0
OK
127.0.0.1:6379> keys *
(empty array)
2.5 Redis 的线程模型
Redis4.0之前是单线程运行的;
Redis4.0后开始支持多线程。Redis4.0之前使用单线程的原因:
- 单线程模式方便开发和调试;
- Redis内部使用了基于epoll的多路复用;
- Redis主要的性能瓶颈是内存或网络带宽。
Redis 在 4.0 以及之后的版本中引入了惰性删除(也叫异步删除),意思就是我们可以使用异步的方式对 Redis 中的数据进行删除操作,例如:
- unlink key:和 del key 类似,删除指定的 key,若 key 不存在则 key 被跳过。但是 del 会产生阻塞,而 unlink 命令会在另一个线程中回收内存,即它是非阻塞的。unlink key API;
- flushdb async:删除当前数据库的所有数据flushdb API;
- flushall async:删除所有库中的数据flushall API。
这样处理的好处是不会使Redis的主线程卡顿,会把这些操作交给后台线程来执行。
Redis主线程既然是单线程,为什么还这个快?
- 基于内存操作:Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以它的性能比较高。
- 数据结构简单:Redis 的数据结构比较简单,是为 Redis 专门设计的,而这些简单的数据结构的查找和操作的时间复杂度都是O(1)。
- 多路复用和非阻塞 IO:Redis 使用 IO 多路复用功能来监听多个 socket 连接的客户端,这样就可以使用一个线程来处理多个情况,从而减少线程切换带来的开销,同时也避免了 IO 阻塞操作,从而大大提高了 Redis 的性能。
- 避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的开销,而且单线程不会导致死锁的问题发生。
IO 多路复用是什么?
- 套接字的读写方法默认是阻塞的,例如当调用读取操作read方法时,缓冲区没有任何数据,那么这个线程会卡在这里,直到缓冲区有数据或者连接被关闭时,read方法才会返回,该线程才能继续处理其他业务。
- Redis 使用的时非阻塞的 IO,IO 的读写流程不再是阻塞的,读写方法都是瞬间完成并且返回的,也就是它会采用能读多少就读多少、能写多少就写多少的策略来执行 IO 操作,这显然更符合我们对性能的追求。
- 使用IO多路复用最简单的方式就是使用select函数,此函数是操作系统提供给用户程序的 API 接口,用于监控多个文件描述符(FD)的可读和可写情况的,这样就可以监控到文件描述符的读写事件了。当监控到相应的事件之后就可以通知线程处理相应的业务了,这样就保证了 Redis 读写功能的正常执行。
Redis6.0中的多线程?
- Redis单线程的优点非常,不但降低了 Redis 内部实现的复杂性,也让所有操作都可以在无锁的情况下进行,并且不存在死锁和线程切换带来的性能以及时间上的消耗;但是其缺点也很明显,单线程机制导致 Redis 的 QPS(Query Per Second,每秒查询数)很难得到有效的提高。
- Redis 在 4.0 版本中虽然引入了多线程,但是此版本的多线程只能用于大数据量的异步删除,对于非删除操作的意义并不是很大。
- 在 Redis 中虽然使用了 IO 多路复用,并且是基于非阻塞的 IO 进行操作的,但是 IO 的读写本身是阻塞的。比如当 socket 中有数据时,Redis 会先将数据从内核态空间拷贝到用户态空间,然后再进行相关操作,而这个拷贝过程是阻塞的,并且当数据量越大时拷贝所需要的的时间就越多,而这些操作都是基于单线程完成的。
- 因此在 Redis6.0 中新增了多线程的功能来提高 IO 的读写性能,它的主要实现思路是将主线程的 IO 读写任务拆分给一组独立的线程去执行,这样就可以使用多个 socket 的读写并行化了,但 Redis 的命令依旧是主线程串行执行 的。
- 注意:Redis6.0 是默认禁用多线程的,但可以通过配置文件 redis.conf 中的 io-threads-do-reads 等于 true 来开启。但是还不够,除此之外我们还需要设置线程的数量才能正确地开启多线程的功能,同样是修改 Redis 的配置,例如设置 io-threads 4,表示开启4个线程。
- 关于线程数的设置,官方的建议是如果为 4 核 CPU,那么设置线程数为 2 或 3;如果为 8 核 CPU,那么设置线程数为 6 。总之线程数一定要小于机器的 CPU 核数,线程数并不是越大越好。
3 五大数据类型
3.1 Redis-Key
127.0.0.1:6379> keys # 查看当前数据库所有 key
(empty array)
127.0.0.1:6379> set name zhangsan # set key
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> set age 2
OK
127.0.0.1:6379> keys *
1) "name"
2) "age"
127.0.0.1:6379> EXISTS name # 判断当前的 key 是否存在
(integer) 1
127.0.0.1:6379> MOVE name 1 # 移除当前 key
(integer) 1
127.0.0.1:6379> KEYS *
1) "age"
127.0.0.1:6379> set name lisi
OK
127.0.0.1:6379> KEYS *
1) "name"
2) "age"
127.0.0.1:6379> get name
"lisi"
127.0.0.1:6379> EXPIRE name 10 # 设置 key 的过期时间,单位是秒
(integer) 1
127.0.0.1:6379> TTL name # 查看当前 key 的剩余时间
(integer) 5
127.0.0.1:6379> TTL name
(integer) 1
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> type name # 查看当前 key 的一个类型
none
127.0.0.1:6379> type age
string
可以查看官网帮助文档:http://www.redis.cn/commands.html
3.2 string(字符串)
#############################################################
127.0.0.1:6379> set key1 v1 # 设置
OK
127.0.0.1:6379> get key1
"v1"
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> EXISTS key1
(integer) 1
127.0.0.1:6379> APPEND key1 "hello" # 追加
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> STRLEN key1 # 字符串长度
(integer) 7
127.0.0.1:6379> APPEND key1 ",zhangsan"
(integer) 16
127.0.0.1:6379> STRLEN key1
(integer) 16
127.0.0.1:6379> get key1
"v1hello,zhangsan"
#############################################################
# i++
# 步长 i +=
127.0.0.1:6379> SET views 0 # 初始浏览量0
OK
127.0.0.1:6379> GET views
"0"
127.0.0.1:6379> INCR views # 自增1 浏览量变为+1
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> DECR views # 自减1 浏览量变为-1
(integer) 1
127.0.0.1:6379> DECR views
(integer) 0
127.0.0.1:6379> DECR views
(integer) -1
127.0.0.1:6379> GET views
"-1"
127.0.0.1:6379> INCRBY views 10 # 可以设置步长 指定增量
(integer) 9
127.0.0.1:6379> INCRBY views 10
(integer) 19
127.0.0.1:6379> DECRBY views 5
(integer) 14
#############################################################
# 字符串范围 range
127.0.0.1:6379> SET key1 "hello,zhangsan"
OK
127.0.0.1:6379> get key1
"hello,zhangsan"
127.0.0.1:6379> GETRANGE key1 0 3
"hell"
127.0.0.1:6379> GETRANGE key1 0 -1
"hello,zhangsan"
127.0.0.1:6379> SET key2 abcdefg
OK
# 替换字符
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> SETRANGE key2 1 xx
(integer) 7
127.0.0.1:6379> get key2
"axxdefg"
#############################################################
# setex (set with expire) 设置过期时间
# setnx (set if not exist) 不存在在设置 (在分布式锁中会常常使用!)
127.0.0.1:6379> SETEX key3 30 "hello" # 设置 key3 的值为 hello,30 秒后过期
OK
127.0.0.1:6379> ttl key3
(integer) 22
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> setnx key4 "redis" # 如果 key4 不存在则创建
OK
127.0.0.1:6379> keys *
1) "key1"
2) "key4"
3) "key2"
127.0.0.1:6379> ttl key3 # -2 表示已失效
(integer) -2
127.0.0.1:6379> setnx key4 "MongoDB" # 存在 key4 创建失败
(integer) 0
127.0.0.1:6379> get key4
"redis"
#############################################################
# mset 同时设置多个值
# mget 同时获取多个值
127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3 # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 # 同时设置不存在的值,原子操作,一个失败全部失败
(integer) 0
127.0.0.1:6379> get key4
(nil)
#############################################################
# 对象
127.0.0.1:6379> set user:1 {name:zhangsan,age:3} # 设置一个user:1 对象 值为 json字符来保存一个对象
OK
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2 # key: user:{id}:{filed}
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
#############################################################
# getset 先get然后在set
127.0.0.1:6379> getset db redis # 如果不存在值,则返回 nil,并设置值
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongodb # 如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db
"mongodb"
string 的使用场景:value 除了是我们的字符串还可以是我们的数字。
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储
3.3 list(列表)
在 redis 里面,我们可以把 list 玩成 ,栈、队列、阻塞队列。
所有的 list 命令都是用 l 开头的,Redis 不区分大小命令。
# LPUSH
# RPUSH
127.0.0.1:6379> LPUSH list one # 头插,向左边插入一个或多个值
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list tree
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1 # 获取 list 中的值
1) "tree"
2) "two"
3) "one"
127.0.0.1:6379> LRANGE list 0 1 # 获取 list 指定范围的值,[start, end]
1) "tree"
2) "two"
127.0.0.1:6379> RPUSH list right # 尾插,向右边插入一个或多个值
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "tree"
2) "two"
3) "one"
4) "right"
#############################################################
# LPOP 移除头部元素
# RPOP 移除尾部元素
127.0.0.1:6379> LRANGE list 0 -1
1) "tree"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379> LPOP list # 移除头部第一个元素
"tree"
127.0.0.1:6379> RPOP list # 移除尾部第一个元素
"right"
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379>
#############################################################
# LINDEX 获取指定索引的值
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LINDEX list 1
"one"
127.0.0.1:6379> LINDEX list 0
"two"
#############################################################
# LLEN 获取集合 size
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LLEN list # 列表长度
(integer) 2
#############################################################
# LREM 移除指定的值! 取关 uid
127.0.0.1:6379> lpush list one two three three
(integer) 4
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> LREM list 1 one # 移除 list 集合中指定个数的 value,精确匹配
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> LREM list 2 three
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
#############################################################
# LTRIM 截断
127.0.0.1:6379> RPUSH list hello hello1 hello2 hello3
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379> LTRIM list 1 2 # 通过下标截取指定的长度,这个 list 已经被改变了,截断了 只剩下截取的元素!
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "hello1"
2) "hello2"
#############################################################
# rpoplpush 移除列表的最后一个元素,将他移动到新的列表中
127.0.0.1:6379> RPUSH list hello hello1 hello2
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1
1) "hello"
2) "hello1"
3) "hello2"
127.0.0.1:6379> RPOPLPUSH list list2 # 移除 list 列表的最后一个元素,将他移动到新的列表 list2 中
"hello2"
127.0.0.1:6379> LRANGE list 0 -1
1) "hello"
2) "hello1"
127.0.0.1:6379> LRANGE list2 0 -1
1) "hello2"
#############################################################
# LSET 将列表中指定下标的值替换为另外一个值,更新操作
127.0.0.1:6379> EXISTS list
(integer) 0
127.0.0.1:6379> LSET list 0 item # 如果不存在列表我们去更新就会报错
(error) ERR no such key
127.0.0.1:6379> LPUSH list one
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "one"
127.0.0.1:6379> LSET list 0 item # 如果存在,更新当前下标的值
OK
127.0.0.1:6379> LRANGE list 0 0
1) "item"
127.0.0.1:6379> LSET list 1 two # 索引越界
(error) ERR index out of range
#############################################################
# LINSERT 将某个具体的value插入到列把你中某个元素的前面或者后面
127.0.0.1:6379> RPUSH list hello world
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "hello"
2) "world"
127.0.0.1:6379> LINSERT list before world other # 在 world 前面插入 other
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379> LINSERT list after world new # 在 world 后面插入 new
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "hello"
2) "other"
3) "world"
4) "new"
他实际上是一个链表,before Node after ,left,right 都可以插入值。如果 key 不存在,创建新的链表,如果 key 存在,新增内容。如果移除了所有值,空链表,也代表不存在。在两边插入或者改动值,效率最高。 中间元素,相对来说效率会低一点。
应用:消息排队,消息队列 (Lpush Rpop), 栈( Lpush Lpop)。
3.4 set(集合)
set中的值是不能重复的!
127.0.0.1:6379> SADD myset hello # 向 set 集合添加一个或多个元素
(integer) 1
127.0.0.1:6379> SADD myset hello1 hello2
(integer) 2
127.0.0.1:6379> SMEMBERS myset # 查看 set 的值
1) "hello2"
2) "hello1"
3) "hello"
127.0.0.1:6379> SISMEMBER myset hello # 判断某一值是否在 set 集合中
(integer) 1
127.0.0.1:6379> SISMEMBER myset world
(integer) 0
#############################################################
127.0.0.1:6379> SCARD myset # 查看 set 集合元素个数
(integer) 3
#############################################################
127.0.0.1:6379> SREM myset hello # 移除 set 集合中的指定元素
(integer) 1
127.0.0.1:6379> SCARD myset
(integer) 2
127.0.0.1:6379> SMEMBERS myset
1) "hello2"
2) "hello1"
#############################################################
# set 无序不重复集合。抽随机
127.0.0.1:6379> SADD myset one two three
(integer) 3
127.0.0.1:6379> SMEMBERS myset
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> SRANDMEMBER myset # 随机抽选出一个元素
"two"
127.0.0.1:6379> SRANDMEMBER myset
"three"
127.0.0.1:6379> SRANDMEMBER myset 2 # 随机抽选出指定个数的元素
1) "three"
2) "one"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "two"
2) "one"
#############################################################
# 删除指定的 key,随机的 key
127.0.0.1:6379> SMEMBERS myset
1) "four"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> SREM myset two # 删除指定的元素
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "four"
2) "three"
3) "one"
127.0.0.1:6379> SPOP myset # 随机删除元素
"one"
127.0.0.1:6379> SPOP myset
"four"
127.0.0.1:6379> SMEMBERS myset
1) "three"
#############################################################
# 将一个指定的值,移动到另外一个set集合
127.0.0.1:6379> SADD myset one two three
(integer) 3
127.0.0.1:6379> SADD myset1 four
(integer) 1
127.0.0.1:6379> SMOVE myset myset1 one # 将一个指定的值,移动到另外一个 set 集合
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "three"
2) "two"
127.0.0.1:6379> SMEMBERS myset1
1) "four"
2) "one"
##########################################################################
# SDIFF 差集
# SINTER 交集
# SUNION 并集
127.0.0.1:6379> SADD key1 a b c d
(integer) 4
127.0.0.1:6379> SMEMBERS key1
1) "d"
2) "c"
3) "b"
4) "a"
127.0.0.1:6379> SADD key2 b c
(integer) 2
127.0.0.1:6379> SMEMBERS key2
1) "c"
2) "b"
127.0.0.1:6379> SDIFF key1 key2
1) "a"
2) "d"
127.0.0.1:6379> SINTER key1 key2
1) "c"
2) "b"
127.0.0.1:6379> SUNION key1 key2
1) "d"
2) "c"
3) "b"
4) "a"
3.5 hash(哈希)
Map集合,key-map 时候这个值是一个 map 集合! 本质和String类型没有太大区别,还是一个简单的 key-(key-vlaue)。
# hset key field value
127.0.0.1:6379> HSET myhash f1 v1 # set 一个具体的 key-value
(integer) 1
127.0.0.1:6379> HGET myhash f1 # 获取一个字段的值
"v1"
127.0.0.1:6379> HMSET myhash f2 v2 f3 v3 # set 多个具体的 key-value
OK
127.0.0.1:6379> HMGET myhash f2 f3 # 获取多个字段的值
1) "v2"
2) "v3"
127.0.0.1:6379> HGETALL myhash # 获取全部的数据
1) "f1"
2) "v1"
3) "f2"
4) "v2"
5) "f3"
6) "v3"
127.0.0.1:6379> HDEL myhash f1 # 删除 hash 指定的 key,相应的 value 也会删除
(integer) 1
127.0.0.1:6379> HGETALL myhash
1) "f2"
2) "v2"
3) "f3"
4) "v3"
127.0.0.1:6379> HLEN myhash # 获取 hash 字段数量
(integer) 2
127.0.0.1:6379> HEXISTS myhash f2 # 判断 hash 中指定字段是否存在
(integer) 1
127.0.0.1:6379> HEXISTS myhash f1
(integer) 0
127.0.0.1:6379> HKEYS myhash # 获取 hash 所有 field
1) "f2"
2) "f3"
127.0.0.1:6379> HVALS myhash # 获取 hash 所有 value
1) "v2"
2) "v3"
##########################################################################
127.0.0.1:6379> HSET myhash field1 5
(integer) 1
127.0.0.1:6379> HINCRBY myhash field1 1 # 指定增量
(integer) 6
127.0.0.1:6379> HINCRBY myhash field1 -1
(integer) 5
127.0.0.1:6379> HSETNX myhash field2 hello # 如果不存在则可以设置
(integer) 1
127.0.0.1:6379> HSETNX myhash field2 world # 如果存在则不能设置
(integer) 0
hash 时候存储变更的数据,尤其是用户信息之类的,经常变动的信息! hash 更适合于对象的存储,string 更加适合字符串存储
3.6 zset(有序集合)
在set的基础上,增加了一个值,set k1 v1 zset k1 score1 v1
127.0.0.1:6379> zadd mzset 1 one # 添加一个值
(integer) 1
127.0.0.1:6379> zadd mzset 2 two 3 three # 添加多个值
(integer) 2
127.0.0.1:6379> zrange mzset 0 -1
1) "one"
2) "two"
3) "three"
##########################################################################
# 如何排序
127.0.0.1:6379> ZADD salary 2500 zhangsan
(integer) 1
127.0.0.1:6379> ZADD salary 5000 lisi
(integer) 1
127.0.0.1:6379> ZADD salary 4000 wangwu
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 负无穷到正无穷升序 by score
1) "zhangsan"
2) "wangwu"
3) "lisi"
127.0.0.1:6379> ZREVRANGEBYSCORE salary +inf -inf # 正无穷到负无穷降序 by score
1) "lisi"
2) "wangwu"
3) "zhangsan"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 显示全部的用户并且附带成 绩withscores
1) "zhangsan"
2) "2500"
3) "wangwu"
4) "4000"
5) "lisi"
6) "5000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 4000 withscores # 显示工资小于等于 4000 员工的升序排序
1) "zhangsan"
2) "2500"
3) "wangwu"
4) "4000"
##########################################################################
# zrem 移除
127.0.0.1:6379> ZRANGE salary 0 -1
1) "zhangsan"
2) "wangwu"
3) "lisi"
127.0.0.1:6379> ZREM salary lisi # 移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> ZRANGE salary 0 -1
1) "zhangsan"
2) "wangwu"
127.0.0.1:6379> ZCARD salary # 获取有序集合中的个数
(integer) 2
##########################################################################
127.0.0.1:6379> ZADD myset 1 hello 2 world 3 lisi
(integer) 3
127.0.0.1:6379> ZCOUNT myset 0 2 ## 获取指定区间的成员数量
(integer) 2
127.0.0.1:6379> ZCOUNT myset 1 3
(integer) 3
127.0.0.1:6379> ZCOUNT myset 1 2
(integer) 2
127.0.0.1:6379> ZADD myset 1 hello2
(integer) 1
127.0.0.1:6379> ZCOUNT myset 1 3
(integer) 4
应用场景:
- set 排序 存储班级成绩表,工资表排序。
- 普通消息,1, 重要消息 2,带权重进行判断。
- 排行榜应用实现,取 Top N。
4 三种特殊数据类型
4.1 geospatial(地理位置)
这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人。
官方文档:https://www.redis.net.cn/order/3685.html
相关命令:
- GEOADD:添加
- GEOPOS:获取位置
- GEODIST:两个位置之间的距离
- GEORADIUS: 以给定的经纬度为中心, 找出某一半径内的元素
- GEORADIUSBYMEMBER:找出位于指定元素周围的其他元素
- GEOHASH:返回一个或多个位置元素的 Geohash 表示
####### GEOADD 添加一个或多个地址位置信息
# GEOADD key longitude latitude member []
# 规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入
# 有效的经度从-180度到180度
# 有效的纬度从-85.05112878度到85.05112878度
# 当坐标位置超出上述指定范围时,该命令将会返回一个错误
127.0.0.1:6379> GEOADD china:city 120.19 30.26 hangzhou
(integer) 1
127.0.0.1:6379> GEOADD china:city 116.46 39.92 beijing
(integer) 1
127.0.0.1:6379> GEOADD china:city 121.48 31.22 shanghai
(integer) 1
127.0.0.1:6379> GEOADD china:city 113.23 23.16 guangzhou 102.73 25.04 kunming
(integer) 2
####### GEOPOS 获取一个地理位置信息
# GEOPOS key member
127.0.0.1:6379> GEOPOS china:city beijing
1) 1) "116.45999997854232788"
2) "39.9199990416181052"
127.0.0.1:6379> GEOPOS china:city hangzhou
1) 1) "120.18999785184860229"
2) "30.25999927289620928"
####### GEODIST 两个位置之间的距离
# GEODIST key member1 member2 unit
# 单位 m 表示单位为米。km 表示单位为千米。mi 表示单位为英里。ft 表示单位为英尺。
127.0.0.1:6379> GEODIST china:city beijing shanghai km
"1068.4581"
# GEORADIUS 以给定的经纬度为中心, 找出某一半径内的元素
# GEORADIUS key longitude latitude radius unit [...]
127.0.0.1:6379> GEORADIUS china:city 120 30 500 km # 这个经纬度为中心,寻找方圆 500km 内的城市
1) "hangzhou"
2) "shanghai"
127.0.0.1:6379> GEORADIUS china:city 120 30 500 km withdist # 显示距离
1) 1) "hangzhou"
2) "34.2105"
2) 1) "shanghai"
2) "196.1728"
127.0.0.1:6379> GEORADIUS china:city 120 30 500 km withcoord # 显示他人的位置信息
1) 1) "hangzhou"
2) 1) "120.18999785184860229"
2) "30.25999927289620928"
2) 1) "shanghai"
2) 1) "121.48000091314315796"
2) "31.21999956478423854"
127.0.0.1:6379> GEORADIUS china:city 120 30 500 km withcoord count 1 # 筛选个数
1) 1) "hangzhou"
2) 1) "120.18999785184860229"
2) "30.25999927289620928"
# GEORADIUSBYMEMBER 找出位于指定元素周围的其他元素
# GEORADIUSBYMEMBER key member radius unit [...]
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 400 km
1) "beijing"
# GEOHASH 返回一个或多个位置元素的 Geohash 表示,将返回11个字符的Geohash字符串
# GEOHASH member [member]
# 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近
127.0.0.1:6379> GEOHASH china:city beijing shanghai
1) "wx4g455wfe0"
2) "wtw3s77j9j0"
geo 底层的实现原理其实就是 zset!我们可以使用zset 命令来操作 geo。
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "kunming"
2) "guangzhou"
3) "hangzhou"
4) "shanghai"
5) "beijing"
127.0.0.1:6379> ZREM china:city beijing
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "kunming"
2) "guangzhou"
3) "hangzhou"
4) "shanghai"
4.2 hyperloglogs(基数)
A {1,3,5,7,8,7}
B{1,3,5,7,8}
基数(不重复的元素) = 5,存在一定误差。
优点:占用的内存是固定,2^64 不同的元素,只需要废 12KB 内存。
网页的 UV (一个人访问一个网站多次,但是还是算作一个人):传统的方式, set 保存用户的id,然后就可以统计 set 中的元素数量作为标准判断 。这个方式如果保存大量的用户id,就会浪费内存。
0.81% 错误率,统计UV任务,可以忽略不计。
127.0.0.1:6379> PFADD key1 a b c d e f g h i j # 创建第一组元素 key1
(integer) 1
127.0.0.1:6379> PFCOUNT key1 # 统计 key1 元素的基数数量
(integer) 10
127.0.0.1:6379> PFADD key2 i j k l m n # 创建第二组元素 key2
(integer) 1
127.0.0.1:6379> PFCOUNT key2 # 统计 key2 元素的基数数量
(integer) 6
127.0.0.1:6379> PFMERGE key3 key1 key2 # 合并两组 key1 key2 => key3 并集
OK
127.0.0.1:6379> PFCOUNT key3 # # 看并集的数量
(integer) 14
如果允许容错,则可以使用 hyperloglog 统计数量
4.3 bitmaps(位图)
统计用户信息,活跃,不活跃;登录 、 未登录;打卡,365打卡记录;两个状态的,都可以使用 bitmaps。
bitmap 位图,数据结构,操作二进制位来进行记录,就只有0 和 1 两个状态。
使用 bitmap 来记录 周一到周日的打卡!
周一:1 周二:0 周三:0 周四:1 …
127.0.0.1:6379> SETBIT sign 0 1
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0
(integer) 0
127.0.0.1:6379> SETBIT sign 2 0
(integer) 0
127.0.0.1:6379> SETBIT sign 3 1
(integer) 0
127.0.0.1:6379> SETBIT sign 4 1
(integer) 0
127.0.0.1:6379> SETBIT sign 5 0
(integer) 0
127.0.0.1:6379> SETBIT sign 6 0
(integer) 0
127.0.0.1:6379> GETBIT sign 3 # 查看某一天是否有打卡记录
(integer) 1
127.0.0.1:6379> GETBIT sign 6
(integer) 0
127.0.0.1:6379> BITCOUNT sign # 统计操作,统计打卡天数
(integer) 3