一、NoSQL
1、数据库发展
1. 单机MySQL
90年代,一个网站的访问量一般不会太大,单个数据库完全够用。随着用户增多,网站出现以下问题:
- 数据量增加到一定程度,单机数据库就放不下了
- 数据的索引(B + Tree),一个机器内存也存放不下
- 访问量变大后(读写混合),一台服务器承受不住。
2. 读写分离
- Memcached(缓存) + MySQL + 垂直拆分
网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据库的压力,我们可以使用缓存来保证效率!优化过程经历了以下几个过程:
- 优化数据库的数据结构和索引(难度大)
- 文件缓存,通过IO流获取比每次都访问数据库效率略高,但是流量爆炸式增长时候,IO流也承受不了
- MemCache,当时最热门的技术,通过在数据库和数据库访问层之间加上一层缓存,第一次访问时查询数据库,将结果保存到缓存,后续的查询先检查缓存,若有直接拿去使用,效率显著提升。
3. 分库分表
- 分库分表 + 水平拆分 + MySQL集群
4. 如今年代
如今信息量井喷式增长,各种各样的数据出现(用户定位数据,图片数据等),大数据的背景下关系型数据库(RDBMS)无法满足大量数据要求。NoSQL数据库就能轻松解决这些问题。
2、NoSQL特点
SQl | NoSQL | |
---|---|---|
数据结构 | 结构化(Structured) | 非结构化 |
数据关联 | 关联的(Relational) | 无关联的 |
查询方式 | SQL查询 | 非SQL |
事务特性 | ACID | BASE |
存储方式 | 磁盘 | 内存 |
拓展性 | 垂直 | 水平 |
使用场景 | 数据结构固定、数据安全性高、一致性高 | 数据结构不固定、一致性安全性不高、性能要求高 |
3、NoSQL特点
- 数据之间没有关系,方便拓展
- 大数据量高性能(Redis一秒可以写8万次,读11万次,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高)
- 数据类型是多样型的(不需要事先设计数据库,随取随用)
4、四大NoSQL分类
1. KV键值对
- 新浪:Redis
- 美团:Redis + Tair
- 阿里、百度:Redis + memecache
2. 文档型数据库
- MongoDB (一般必须要掌握)
- MongoDB 是一个基于分布式文件存储的数据库,C++ 编写,主要用来处理大量的文档!
- MongoDB 是一个介于关系型数据库和非关系型数据中中间的产品!
- MongoDB 是非关系型数据库中功能最丰富,最像关系型数据库的!
- ConthDB
3. 列存储数据库
- HBase
- 分布式文件系统
4. 图关系数据库
-
他不是存图形,放的是关系,比如:朋友圈社交网络,广告推荐!
-
Neo4j,InfoGrid;
类 | Example举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 |
---|---|---|---|---|---|
键值(key-value) | Tokyo Cabine /Tyrant,redis,Voldemort,Oracle,BDB | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。 | key指向Value的键值对,通常用HashTable来实现 | 查找速度快 | 数据无结构化,通常制备当作字符串或者二进制数据。 |
列存储数据库 | Cassandra,HBase,Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
文档型数据库 | Couch DB,MongoDB | Web应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解value的内容) | key-value对键值对,value为结构化数据 | 数据结构要求不严格,表结构可边,不需要像关系型数据库一样需要预先定义表结构 | 查询性能不高,而且缺乏统一的查询语法 |
图形数据库 | Neo4j,InfoGrid,Infinite Graph | 社交网络,推荐系统等,专注于构建关系图谱 | 图结构 | 利用图结构相关算法,比如最短路径寻址,N度关系查找等。 | 很多时候需要对整个图做计算才能得出需要的计算信息,而且这种结构不太好做分布式的集群方案 |
二、Redis入门
1、Redis介绍
- Redis(Remote Dictionary Server),即远程字典服务
- 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
免费和开源!是当下最热门的NoSQL技术之一,也被人们称为结构化数据库。
2、Redis特征
- Redis是单线程的
- 键值(key-value)型,value支持多种不同数据结构,功能丰富
- 单线程,每个命令具备原子性
- 低延迟,速度快(基于内存、IO多路复用、良好的编码)
- 支持数据持久化
- 支持主从集群、分片集群
- 支持多语言客户端
3、Redis通用命令
- keys:查看符合模板的所有key,不建议在生产环境设备上使用
- del:删除一个指定的key
- exists:判断key是否存在
- expire:给一个key设置有效期,有效期到期时该key会被自动删除
- ttl:查看一个KEY的剩余有效期
127.0.0.1:6379> select 3 # 切换数据库
OK
127.0.0.1:6379[3]> dbsize # 查看数据库大小
(integer) 0
127.0.0.1:6379> select 0 # 切换数据库
OK
127.0.0.1:6379> set name zhangsan # 储存 key
OK
127.0.0.1:6379> get name # 获取 key
"zhangsan"
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> keys * # 查看所有的 key
1) "myset:__rand_int__"
2) "counter:__rand_int__"
3) "name"
4) "mylist"
5) "key:__rand_int__"
127.0.0.1:6379> expire name 10 # 设置 key 的过期时间,单位是秒
(integer) 1
127.0.0.1:6379> ttl name # 查看 key 的剩余时间,单位是秒
(integer) -2
127.0.0.1:6379> flushdb # 清空当前数据库
OK
127.0.0.1:6379> keys * # 查看所有的 key
(empty list or set)
127.0.0.1:6379> flushall # 清空所有数据库
OK
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> exists name # 判断 key 是否存在
(integer) 1
127.0.0.1:6379> type name # 查看 key 的类型
string
127.0.0.1:6379> move name 1 # 移除 key 1代表当前数据库
(integer) 1
127.0.0.1:6379> keys *
(empty list or set)
三、五大数据类型
- Redis 是一个开源( BSD 许可)的,内存中的数据结构存储系统,它可以用作数据库 、缓存和消息中间件MQ。
- 它支持多种类型的数据结构,如 字符串( strings ), 散列( hashes ), 列表( lists ), 集合 ( sets ), 有序集合( sorted sets ) 与范围查询, bitmaps , hyperloglogs 和 地理空间 ( geospatial ) 索引半径查询。
- Redis 内置了 复制( replication ), LUA 脚本( Lua scripting ), LRU 驱动事件( LRU eviction ),事务( transactions ) 和不同级别的 磁盘持久化( persistence ), 并通过 Redis 哨兵( Sentinel )和自动 分区( Cluster )提供高可用性( high availability )。
1、String(字符串)
-
String类型,也就是字符串类型,是Redis中最简单的存储类型
-
其value是字符串,不过根据字符串的格式不同,又可以分为3类:
- string:普通字符串
- int:整数类型,可以做自增、自减操作
- lfloat:浮点类型,可以做自增、自减操作
-
不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同。字符串类型的最大空间不能超过512m.
1. 基本操作
127.0.0.1:6379> set key1 hello -- 设置值
OK
127.0.0.1:6379> get key1 -- 获取值
"hello"
127.0.0.1:6379> append key1 " world" -- 追加内容,如果不存在相当于 set key
(integer) 11
127.0.0.1:6379> get key1
"hello world"
127.0.0.1:6379>
127.0.0.1:6379> strlen key1 -- 获取字符串的长度
(integer) 11
2. 自增长
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> get views
"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> incrby views 5
(integer) 14
3. 范围截取
127.0.0.1:6379> set key1 hello,world
OK
127.0.0.1:6379> get key1
"hello,world"
127.0.0.1:6379> getrange key1 0 4 -- 截取字符串 [0,3]
"hello"
127.0.0.1:6379> getrange key1 0 -1 -- 获取全部的字符串 同 get key
"hello,world"
127.0.0.1:6379>
4. 替换
127.0.0.1:6379> set key1 hello,world
OK
127.0.0.1:6379> get key1
"hello,world"
127.0.0.1:6379> setrange key1 6 java -- 替换指定位置开始的字符串
(integer) 11
127.0.0.1:6379> get key1
"hello,javad"
5. 设置过期时间
# setex (set with expire) -- 设置过期时间
# setnx (set if not exist) -- 如果 key 不存在,再设置(在分布式锁中会常常使用)
127.0.0.1:6379> setex key3 30 "hello" -- 设置key3 的值为 hello,30秒后过期
OK
127.0.0.1:6379> ttl key3
(integer) 26
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> setnx mykey "redis" -- 如果mykey 不存在,创建 mykey
(integer) 1
127.0.0.1:6379> get mykey
"redis"
127.0.0.1:6379> setnx mykey "MongoDB" -- 如果mykey存在,创建失败!
(integer) 0
127.0.0.1:6379> get mykey
"redis"
6. 批量操作
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 -- 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
3) "k3"
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 -- msetnx 是一个原子性的操作,要么一起成功,要么一起 失败!
(integer) 0
127.0.0.1:6379> get k4
(nil)
7. 添加对象
127.0.0.1:6379> set user:1 '{"name":"zhangsan","age":"3"}' -- 设置一个user:1 对象
-- 这里的key是一个巧妙的设计: user:{id}:{filed} , 如此设计在 Redis 中是完全OK了!
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2 -- 这样设计无需 json 字符串的转换
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
8. getset
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"
2、List(列表)
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。
特征也与LinkedList类似:
- 有序
- 元素可以重复
- 插入和删除快
- 查询速度一般
常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等:
- 消息排队
- 消息队列(Lpush Rpop)
- 栈(Lpush,Lpop)
1. 添加命令
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 three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 -- 获取list中值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1 -- 通过区间获取具体的值
1) "three"
2) "two"
127.0.0.1:6379> rpush list righr -- 将一个值或者多个值,插入到列表位部 (右)
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "righr"
2. 移除命令
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "righr"
127.0.0.1:6379> lpop list -- 移除list的第一个元素
"three"
127.0.0.1:6379> rpop list -- 移除list的最后一个元素
"righr"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3. 通过下标获取
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex list 1 -- 通过下标获得 list 中的某一个值!
"one"
127.0.0.1:6379> lindex list 0
"two"
4. 获取长度
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 three
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> llen list -- 返回列表的长度
(integer) 3
5. 移除指定的值
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 1 three -- 移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 2 three -- 移除list集合中指定个数的value,精确匹配
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "two"
6. 截断操作
127.0.0.1:6379> rpush mylist "hello" -- 将一个值或者多个值,插入到列表位部 (右)
(integer) 1
127.0.0.1:6379> rpush mylist "hello1" -- 将一个值或者多个值,插入到列表位部 (右)
(integer) 2
127.0.0.1:6379> rpush mylist "hello2" -- 将一个值或者多个值,插入到列表位部 (右)
(integer) 3
127.0.0.1:6379> rpush mylist "hello3" -- 将一个值或者多个值,插入到列表位部 (右)
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379> ltrim mylist 1 2 -- 通过下标截取指定的长度,list只剩下截取的元素
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello1"
2) "hello2"
7. 移除并新建
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> rpush mylist "hello2"
(integer) 3
127.0.0.1:6379> rpoplpush mylist myotherlist -- 移除列表的最后一个元素,将他移动到新的列表中
"hello2"
127.0.0.1:6379> lrange mylist 0 -1 -- 查看原来的列表
1) "hello"
2) "hello1"
127.0.0.1:6379> lrange myotherlist 0 -1 -- 查看目标列表中,确实存在改值!
1) "hello2"
8. 判断是否存在
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 value1
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "value1"
127.0.0.1:6379> lset list 0 item -- 如果存在,更新当前下标的值
OK
127.0.0.1:6379> lrange list 0 -1
1) "item"
127.0.0.1:6379> lset list 1 other -- 如果不存在,则会报错!
(error) ERR index out of range
7. 指定插入
-- linsert 将某个具体的value插入到列表中某个元素的前面或者后面
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "world"
(integer) 2
127.0.0.1:6379> linsert mylist before "world" "other" -- 在 world 前面插入 other
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379> linsert mylist after world new -- 在 world 后面插入 new
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "world"
4) "new"
3、Set(集合)
-
Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:
- 无序
- 元素不可重复
- 查找快
- 支持交集、并集、差集等功能
-
微博, A 用户将所有关注的人放在一个 set 集合中,将它的粉丝也放在一个集合中,共同关注,共同爱好,二度好友,推荐好友。(六度分割理论)
1. 基本操作
127.0.0.1:6379> sadd myset "hello" -- set集合中添加元素
(integer) 1
127.0.0.1:6379> smembers myset -- 查看指定set的所有值
1) "hello"
127.0.0.1:6379> sismember myset hello -- 判断某一个值是不是在set集合中
(integer) 1
127.0.0.1:6379> sismember myset world -- 不存在返回0
(integer) 0
127.0.0.1:6379> scard myset -- 获取set集合中的内容元素个数
(integer) 1
127.0.0.1:6379> srem myset hello -- 移除set集合中的指定元素
(integer) 1
127.0.0.1:6379> smembers myset
(empty list or set)
-- set 无序不重复集合,抽随机。
127.0.0.1:6379> sadd myset hello
(integer) 1
127.0.0.1:6379> sadd myset world
(integer) 1
127.0.0.1:6379> sadd myset java
(integer) 1
127.0.0.1:6379> sadd myset python
(integer) 1
127.0.0.1:6379> sadd myset javaEE
(integer) 1
127.0.0.1:6379> smembers myset
1) "python"
2) "world"
3) "java"
4) "hello"
5) "javaEE"
127.0.0.1:6379> srandmember myset -- 随机抽选出一个元素
"python"
127.0.0.1:6379> srandmember myset -- 随机抽选出一个元素
"hello"
127.0.0.1:6379> srandmember myset -- 随机抽选出一个元素
"javaEE"
127.0.0.1:6379> srandmember myset -- 随机抽选出一个元素
"javaEE"
127.0.0.1:6379> srandmember myset 2 -- 随机抽选出指定个数的元素
1) "world"
2) "javaEE"
127.0.0.1:6379> srandmember myset 3 -- 随机抽选出指定个数的元素
1) "world"
2) "python"
3) "javaEE"
2. 删除操作
-- 删除指定的key,随机删除key
127.0.0.1:6379> smembers myset
1) "python"
2) "world"
3) "java"
4) "hello"
5) "javaEE"
127.0.0.1:6379> spop myset -- 随机删除 set 集合中的元素!
"javaEE"
127.0.0.1:6379> smembers myset
1) "java"
2) "hello"
3) "world"
4) "python"
127.0.0.1:6379> spop myset 2 -- 随机删除 set 集合中的指定个数的元素!
1) "world"
2) "java"
127.0.0.1:6379> smembers myset
1) "hello"
2) "python"
3. 移动集合元素
-- 将一个指定的值,移动到另外一个set集合!
127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sdiff key1 key2
1) "a"
2) "b"
127.0.0.1:6379> sinter key1 key2
1) "c"
127.0.0.1:6379> sunion key1 key2
1) "a"
2) "b"
3) "c"
4) "e"
5) "d"
4. 集合间的操作
-- 集合类:差集 sdiff 、交集 sinter 、并集 sunion
127.0.0.1:6379> sdiff key1 key2 -- 差集
1) "b"
2) "a"
127.0.0.1:6379> sinter key1 key2 -- 交集 共同好友就可以这样实现
1) "c"
127.0.0.1:6379> sunion key1 key2 -- 并集
1) "b"
2) "c"
3) "e"
4) "a"
4、Hash(哈希)
- String结构是将对象序列化为JSON字符串后存储,当需要修改对象某个字段时很不方便
- Hash类型,也叫散列,其value是一个无序字典,类似于Java中的HashMap结构
- Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD:
- hash 更适合存储经常变动的信息,尤其是是用户信息之类的信息
- hash 更适合于对象的 存储, String 更加适合字符串存储
1. 基本操作
127.0.0.1:6379> hset set:user:1 name zhangsan -- set key-vlaue
(integer) 1
127.0.0.1:6379> hset set:user:1 age 5
(integer) 1
127.0.0.1:6379> hget set:user:1 name -- 获取一个字段值
"zhangsan"
127.0.0.1:6379> hget set:user:1 age
"5"
127.0.0.1:6379> hmset set:user:2 name lisi age 18 -- set多个 key-vlaue
OK
127.0.0.1:6379> hmget set:user:2 name age -- 获取多个字段值
1) "lisi"
2) "18"
127.0.0.1:6379> hgetall set:user:1 -- 获取整个字段的数据
1) "name"
2) "zhangsan"
3) "age"
4) "5"
127.0.0.1:6379> hdel set:user:1 name -- 删除hash指定key字段
(integer) 1
127.0.0.1:6379> hgetall set:user:1
1) "age"
2) "5"
2. 获取keys、vals
127.0.0.1:6379> hkeys set:user:2
1) "name"
2) "age"
127.0.0.1:6379> hvals set:user:2
1) "lisi"
2) "18"
3. 获取字段数量
127.0.0.1:6379> hlen set:user:2 -- 获取hash表的字段数量!
(integer) 2
127.0.0.1:6379> hlen set:user:1
(integer) 1
4. 判定字段是否存在
127.0.0.1:6379> hexists set:user:1 name
(integer) 0
127.0.0.1:6379> hexists set:user:1 age
(integer) 1
5. 自增长
127.0.0.1:6379> hincrby set:user:1 age 5
(integer) 10
127.0.0.1:6379> hvals set:user:1
1) "10"
127.0.0.1:6379> hincrby set:user:1 age 5
(integer) 15
127.0.0.1:6379> hvals set:user:1
1) "15"
127.0.0.1:6379> hincrby set:user:1 age -2
(integer) 13
127.0.0.1:6379> hvals set:user:1
1) "13"
6. hsetnx
127.0.0.1:6379> hsetnx set:user:3 name wangwu -- 如果不存在则可以设置
(integer) 1
127.0.0.1:6379> hsetnx set:user:3 name wangwu -- 如果存在则不能设置
(integer) 0
127.0.0.1:6379>
5、Zset(有序集合)
- Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。
- SortedSet具备下列特性:
- 可排序
- 元素不重复
- 查询速度快
- 因为 SortedSet 的可排序特性,经常被用来实现排行榜这样的功能。
1. 基本操作
127.0.0.1:6379> zadd myset 1 one -- 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three -- 添加多个值
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> zcard myset -- 获取有序集合中的个数
(integer) 3
127.0.0.1:6379> zcount myset 1 3 -- 获取指定区间的成员数量
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2
2. 排序
127.0.0.1:6379> zadd salary 2500 xiaohong -- 添加三个用户
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 kaungshen
(integer) 1
# zrangebyscore key min max
127.0.0.1:6379> zrangebyscore salary -inf +inf -- 显示全部的用户 从小到大
1) "kaungshen"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrevrange salary 0 -1 -- 从大到进行排序
1) "zhangsan"
2) "xiaohong"
3) "kaungshen"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores -- 显示全部的用户并且附带成 绩
1) "kaungshen"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores -- 显示工资小于2500员工的升序排序!
1) "kaungshen"
2) "500"
3) "xiaohong"
4) "2500"
3. 移除元素
127.0.0.1:6379> zrange salary 0 -1
1) "kaungshen"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong -- 移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "kaungshen"
2) "zhangsan"
四、Java客户端
序号 | 客户端 | 说明 |
---|---|---|
1 | Jedis | 以Redis命令作为方法名称,学习成本低,简单实用。 但是Jedis实例是线程不安全的,多线程环境下需要基于连接池来使用 |
2 | Lettuce | Lettuce是基于Netty实现的,支持同步、异步和响应式编程方式,并且是线程安全的 支持Redis的哨兵模式、集群模式和管道模式 |
3 | Redisson | Redisson是一个基于Redis实现的分布式、可伸缩的Java数据结构集合。 包含了诸如Map、Queue、Lock、 Semaphore、AtomicLong等强大功能 |
Spring Data Redis:整合 Jedis、Lettuce
1、Jedis
1. 引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2. 入门案例
@SpringBootTest
class JedisTests {
private Jedis jedis;
@Test
void textString() {
// 插入数据
String result = jedis.set("name", "张三");
System.out.println("result = " + result); // result = OK
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name); // name = 张三
}
@Test
void textHash() {
// 插入数据
jedis.hset("user:1", "name", "林青霞");
jedis.hset("user:1", "age", "18");
// 获取数据
Map<String, String> user = jedis.hgetAll("user:1");
System.out.println("user = " + user); // user = {name=林青霞, age=18}
}
@BeforeEach
void setUp() {
// 建立连接
jedis = new Jedis("127.0.0.1", 6379);
// 设置密码
// jedis.auth("123456");
// 选择库
jedis.select(0);
}
@AfterEach
void tearDown() {
// 释放资源
if (jedis != null) {
jedis.close();
}
}
}
2、Jedis连接池
- Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此我们推荐大家使用Jedis连接池代替Jedis的直连方式。
package erer.redis.jedis.factory;
public class JedisConnectionFactory {
private static final JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大连接
jedisPoolConfig.setMaxTotal(8);
// 最大空闲连接
jedisPoolConfig.setMaxIdle(8);
// 最小空闲连接
jedisPoolConfig.setMinIdle(0);
// 设置最长等待时间,单位:ms
jedisPoolConfig.setMaxWaitMillis(200);
jedisPool = new JedisPool(jedisPoolConfig, "localhost", 6379, 1000, null);
}
// 获取Jedis对象
public static Jedis getJedis() {
return jedisPool.getResource();
}
}
3、SpringDataRedis
- SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis
- 提供了对不同Redis客户端的整合(Lettuce和Jedis)
- 提供了 RedisTemplate 统一 API 来操作 Redis
- 支持 Redis 的发布订阅模型
- 支持 Redis 哨兵和Redis集群
- 支持基于 Lettuce 的响应式编程
- 支持基于JDK、JSON、字符串、Spring 对象的数据序列化及反序列化
- 支持基于 Redis 的 JDKCollection 实现
1. RedisTemplate
- SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:
API | 返回值类型 | 说明 |
---|---|---|
redisTemplate.opsForValue() | ValueOperations | 操作String类型数据 |
redisTemplate.opsForHash() | HashOperations | 操作Hash类型数据 |
redisTemplate.opsForList() | ListOperations | 操作List类型数据 |
redisTemplate.opsForSet() | SetOperations | 操作Set类型数据 |
redisTemplate.opsForZSet() | ZSetOperations | 操作SortedSet类型数据 |
redisTemplate | 通用的命令 |
2. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
3. 配置文件
spring:
redis:
host: localhost
port: 6379
lettuce:
pool:
max-active: 8 # 最大连接
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
max-wait: 100 # 连接等待时间
4. 测试代码
@SpringBootTest
class SpringDataRedisTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void testString() {
redisTemplate.opsForValue().set("name", "张曼玉");
Object name = redisTemplate.opsForValue().get("name");
System.out.println(name);
}
}
4、自定义序列化方式
1. 引入依赖
- 非web项目引入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
###2. 自定义RedisTemplate
package erer.redis.springdataredis.config;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 创建Template
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// key 和 hashKey采用 string序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// value 和 hashValue采用 JSON序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
3. POJO
package erer.redis.springdataredis.pojo;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private Integer age;
}
4. 测试代码
@SpringBootTest
class _自定义RedisTemplate {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
void testSaveUser() {
// 写入数据
redisTemplate.opsForValue().set("user:2022", new User("林青霞", 30));
// 获取数据
User user = (User) redisTemplate.opsForValue().get("user:2022");
System.out.println(user); // User(name=林青霞, age=30)
}
}
5、StringRedisTemplate
- 尽管JSON的序列化方式可以满足我们的需求,但依然存在一些问题,为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销。
{
"@class": "erer.redis.springdataredis.pojo.User",
"name": "林青霞",
"age": 30
}
- 为了节省内存空间,我们并不会使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。
- Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了我们自定义RedisTemplate的过程:
class _StringRedisTemplate {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// JSON工具
private static final ObjectMapper mapper = new ObjectMapper();
@Test
void testStringTemplate() throws JsonProcessingException {
// 准备对象
User user = new User("杨幂", 18);
// 手动序列化
String json = mapper.writeValueAsString(user);
// 写入一条数据到redis
stringRedisTemplate.opsForValue().set("user:200", json);
// 读取数据
String val = stringRedisTemplate.opsForValue().get("user:200");
// 反序列化
User user1 = mapper.readValue(val, User.class);
System.out.println("user1 = " + user1); // user1 = User(name=杨幂, age=18)
}
}
6、操作哈希数据
class _StringRedisTemplate {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void testHash(){
stringRedisTemplate.opsForHash().put("user:300", "name", "林青霞");
stringRedisTemplate.opsForHash().put("user:300", "age", "18");
Map<Object, Object> user = stringRedisTemplate.opsForHash().entries("user:300");
System.out.println("user:" + user); // user:{name=林青霞, age=18}
}
}
五、GEO数据结构
- GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。常见的命令有:
- GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
- GEODIST:计算指定的两个点之间的距离并返回
- GEOHASH:将指定member的坐标转为hash字符串形式并返回
- GEOPOS:返回指定member的坐标
- GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.2以后已废弃
- GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
- GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。 6.2.新功能
1、添加地理信息
-- 北京南站( 116.378248 39.865275 )
-- 北京站( 116.42803 39.903738 )
-- 北京西站( 116.322287 39.893729 )
127.0.0.1:6379> geoadd g1 116.378248 39.865275 bjn 116.42803 39.903738 bjz 116.322287 39.893729 bjx
(integer) 3
2、计算两点间距离
-- 计算北京南到北京西的距离
127.0.0.1:6379> geodist g1 bjn bjx
"5729.9533"
127.0.0.1:6379> geodist g1 bjn bjx km
"5.7300"
3、搜索功能
-- 根据距离搜索
127.0.0.1:6379> georadius g1 116.397904 39.909005 10 km withdist
1) 1) "bjz"
2) "2.6361"
2) 1) "bjn"
2) "5.1452"
3) 1) "bjx"
2) "6.6723"
六、Redis网络模型
1、Redis线程
- 如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程
- 如果是聊整个Redis,那么答案就是多线程
在Redis版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:
- Redis v4.0:引入多线程异步处理一些耗时较旧的任务,例如异步删除命令unlink(BigKey)
- Redis v6.0:在核心网络模型中引入 多线程,进一步提高对于多核CPU的利用率
因此,对于Redis的核心网络模型,在Redis 6.0之前确实都是单线程。是利用epoll(Linux系统)这样的IO多路复用技术在事件循环中不断处理客户端情况。
为什么Redis要选择单线程?
- 抛开持久化不谈,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。
- 多线程会导致过多的上下文切换,带来不必要的开销
- 引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣
2、IO多路复用-poll
poll模式对select模式做了简单改进,但性能提升不明显,部分关键代码如下:
IO流程:
- 创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
- 调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
- 内核遍历fd,判断是否就绪
- 数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
- 用户进程判断n是否大于0
- 大于0则遍历pollfd数组,找到就绪的fd
与select对比:
- select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
- 监听FD越多,每次遍历消耗时间也越久,性能反而会下降
3、epoll模式
基于epoll模式的web服务的基本流程如图:
- 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
- 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
- 利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降