文章目录
- 一、前言
- 二、redis基础知识
- 2.1 从“处理器-缓存-内存”到“后台-redis-数据库”
- 2.2 不使用缓存与使用缓存(读操作+写操作)
- 2.3 redis典型问题:缓存穿透、缓存雪崩和缓存击穿(以淘宝双11抢购为例)
- 三、redis五种基本类型的底层结构与应用
- 3.1 redisObject对象(类型type+编码encoding)和sds(free+len+buf)
- 3.2 字符串对象string
- 3.3 列表对象list
- 3.4 哈希对象hash(map)
- 3.5 集合对象set
- 3.6 有序集合对象sortedset
- 3.7 五种基本类型的特点与应用
- 四、Redis七种特殊数据类型
- 4.1 位图Bitmap
- 4.2 HyperLogLog
- 4.3 布隆过滤器
- 4.4 Geospatial
- 4.5 Pub/Sub
- 4.6 Pipeline
- 4.7 Lua脚本
- 4.8 Redis事务
- 4.9 小结
- 五、单机版redis及Java开发实践
- 5.1 单机版Redis安装
- 5.2 Java项目中使用redis
- 5.4 小结(redis单机版)
- 六、集群版redis及Java开发实践
- 6.1 redis集群原理
- 6.2 集群版redis(redis-cluster)安装
- 6.3 Java使用redis集群、Spring容器使用redis集群(jedisCluster)
- 6.4 redis集群节点操作(包括:添加主节点、添加从节点、删除节点)
- 6.5 小结(redis集群)
- 七、尾声
一、前言
redis引入,什么是redis?
Redis 是一个开源的、使用ANSI C编写的、支持网络、基于内存的、可持久化的Key-Value 型的数据库,通过提供多种键值数据类型(5种基本数据类型 string hash list set sortedset)来适应不同场景下的存储需求,并且提供多种语言的API(当然包括Java语言的API)。redis官网如图:
理清几个易混淆的概念
SQL:全称为Structured Query Language,译为结构化查询语言,是一种计算机程序语言,一种解释型语言。
NoSQL:全称Not Only SQL,译为"不仅仅是SQL"(注意,NoSQL不是不使用SQL的意思),泛指所有的非关系型数据库。
关系型数据库:即RDB,全称为Relational Database,其实,一个更加常见的英文简称是RDBMS,Relational Database Management System,关系型数据库管理系统,所以,RDBMS就被认为是关系型数据库的简称。
非关系型数据库:不使用数据库表结构存储数据,用NoSQL表示。
相互对比辨析相近概念
(1) SQL与关系型数据库:SQL是SQL,关系型数据库是关系型数据库,两者是完全不同的两个东西,SQL是一种解释型语言,关系型数据库是数据库的一种类型,两者的关系是关系型数据库的CRUD操作使用SQL语言来完成,SQL语言被认为是关系型数据库的一种特征。
(2) NoSQL与非关系型数据库:对于程序员的工作中,NoSQL就是指非关系型数据库,即NoSQL==非关系型数据库,两者是同一个东西。
(3) SQL与NoSQL:SQL是一种语言,NoSQL表示非关系型数据库,一个是语言,一个是数据库,两个不同关系。
关系型数据库与非关系型数据库,如下表:
关系型数据库RDBMS | 非关系型数据库NoSQL | |
存储格式支持 | 数据库表结构 | 不使用数据库表结构存储,包括列存储、文档存储、key-value存储、图存储、对象存储、xml存储 |
特点 | 高度组织化结构化数据; 结构化查询语言(SQL); 数据和关系都存储在单独的表中; 数据操纵语言,数据定义语言; 严格的一致性; 基础事务 | 代表着不仅仅是SQL; 没有声明性查询语言; 没有预定义的模式; 键值对存储,列存储,文档存储,图形数据库; 最终一致性,而非ACID属性; 非结构化和不可预知的数据; CAP定理; 高性能,高可用性和可伸缩性 |
设计原则 | ACID:A (Atomicity) 原子性、C (Consistency) 一致性、I (Isolation) 独立性、D (Durability) 持久性,表示任何一个关系型数据库(使用表格式存储的数据库)必须同时满足四个特性要求 | BASE原则(同时满足CAP中的CA) Basically Availble --基本可用; Soft-state --软状态/柔性事务。 “Soft state” 可以理解为"无连接"的, 而 “Hard state” 是"面向连接"的; Eventual Consistency – 最终一致性, 也是是 ACID 的最终目的。 |
分类 | Mysql sqlserver oracle | 1)列存储:按列存储数据的,如Hbase、Cassandra、Hypertable 2)文档存储:用类似json的格式存储,存储的内容是文档型的,如MongoDB、CouchDB 3)key-value存储:Tokyo Cabinet / Tyrant、Berkeley DB、MemcacheDB、Redis 4)图存储:图形关系的最佳存储,如Neo4J、FlockDB 5)对象存储:通过对象的方式存取数据,如db4o、Versant 6)xml存储:存储XML数据,如Berkeley DB XML、BaseX |
由上表可知,Redis是一种使用key-value键值对来存储数据的非关系型数据库。
redis与NoSQL的关系:NoSQL可以表示非关系型数据库,redis一种使用key-value键值对存储的非关系型数据库,这就是两者的关系。
redis查询速度快,但是由于是存储在内存中,适用于存放临时、少量的数据,比如验证码有效期5秒;
mongdb查询速度快,但是由于是存储在磁盘上的,适用于存放永久、大量的数据,比如 购物车中的商品。
本文主要包括四个部分的内容,包括redis基础知识、redis底层原理、单机版redis及Java开发实践、集群版redis及Java开发实践。
二、redis基础知识
既然Redis是一种使用key-value键值对来存储数据的非关系型数据库,我们先来介绍这种非关系型数据库的基础知识。
2.1 从“处理器-缓存-内存”到“后台-redis-数据库”
回顾学生年代《计算机组成原理》,由于处理器CPU与内存的速度不匹配问题,所有我们在处理器和内存之间加一个高速缓存,
缓存的数据是主存中热点数据的副本,处理器读取数据时,优先读取缓存中的数据,缓存中没有,再到主存中取,同时这个数据成为热点数据,写入到缓存中,下次处理器直接从缓存中取。
对于写操作,为了保证主存缓存中数据一致性问题,有“写直达法”和“写回法”两种方式。整个架构变化如图:
工作中,web项目开发,由于网络请求与数据库查询数据不匹配问题,所以我们在后台程序与数据库之间加一个redis/redis-cluster缓存,其读写操作与计算机的存储一样,优先读写redis缓存,整个架构变化如下:
这里用硬件对比软件后台,用高速缓存cache对比redis/redis-cluster,两者基本上是一样的,唯一不同的恐怕就是Cache是硬件,redis是软件。
2.2 不使用缓存与使用缓存(读操作+写操作)
在介绍redis读写之前,引入一个知识,redis支持的数据类型(我们起码要知道读写操作,读写的是什么)
redis支持五种数据类型
目前为止Redis支持的键值数据类型一共五种,如下:String字符串类型、hash散列类型、list列表类型、set集合类型、sorted set有序集合类型。
不使用缓存读
不使用缓存写
使用缓存读
使用缓存写(先更新数据库,再更新redis缓存,类似写直达法)
使用缓存写(先更新redis缓存,再更新数据库,类似写回法)
2.3 redis典型问题:缓存穿透、缓存雪崩和缓存击穿(以淘宝双11抢购为例)
2.3.1 缓存穿透,不存在的商品X
从名称上来解释含义:传统意义上的穿透,即水滴石穿,滴水能把石穿透,就是说水滴穿透石头的整个过程。
这里的缓存穿透,是指网络请求查询一个数据库一定不存在的数据(假设为-1,一个无意义数字)。因为这是一个数据库中绝对不存在的数据,所有redis缓存中也一定不存在,执行过程中,因为redis中一定找不到,所有一定会去数据库中找,结果就是数据库也找不到。因为这个网络请求查询过程是 “前端/客户端/移动端—网络请求—redis缓存—数据库” ,整个过程穿透redis,直达数据库,与水滴石穿有类似之意,所以称为缓存穿透。
正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存(这是重点,查不到就不进入缓存,所以第二次请求同样的数据还是要查询数据库)。
缓存穿透的问题再哪里?在于它每次都要请求数据库,redis缓存形同虚设,起不到减少数据库查询、提升性能的作用。
我们知道,每一次查询数据库的代价是比较大的(所以我们引用了redis缓存),因为请求的是一个数据库一定不存在的数据,所有每一次都要查数据库,而且因为数据库查询为空,这次的数据也不会放入缓存,下一次还是查询这个数据又要到数据库中查询,不断循环,一个不存在的数据多次请求就可以让后台系统崩溃。
假如有恶意攻击,就可以利用这个漏洞(网络请求一个数据库中一定不存在的数据,不断请求),对数据库造成压力,甚至压垮数据库。即便是采用UUID,也是很容易找到一个不存在的KEY,进行攻击。
举例:
解决方案:
思考:第一次请求redis中找不到,访问数据库不是什么大问题,后面N-1次都要访问数据库这就是个大问题了。核心在于:如果数据库查询对象为空,则不放进缓存。这是默认规则,如果能消除这条规则,即数据库查询为空也写入redis,后面进直接从redis中取,取不到就结束(因为数据库和redis已经同步了)。
解决:会采用缓存空值null的方式,如果从数据库查询的对象为空,也放入缓存,即将null放入缓存,将key:value=(x,null)写入redis,下一次查询key=x,直接在redis中返回value=null.
2.3.2 缓存雪崩,双十一抢购
从名称上来解释含义:传统意义上的雪崩就是指一种当山坡积雪内部的内聚力抗拒不了它所受到的重力拉引时,便向下滑动,引起大量雪体崩塌的自然现象。
雪崩之所以可怕,是因为其规模之大,局部雪崩可能引起全局雪崩,一次严重的雪崩可能造成整座雪山的崩塌。这里的缓存雪崩是指在某一个时间段,缓存集中过期失效,造成整个redis不可用(对应整座雪山崩塌)。
关于缓存雪崩,粗体标记,注意两个词语,一是“集中”,二是“过期”,一是集中失效,二是过期失效
关于集中:redis默认有16个库,db0~db15,“集中”表示redis缓存中的大部分库是失效了
关于过期:表示redis缓存雪崩中多个库是由于缓存时间到期而失效的
缓存雪崩就是缓存失效,“集中”告诉我们是大部分库失效了,不是小部分或者个别;“过期”是指这种库失效是由于缓存时间到期而失效的(即是正常的失效),不是异常错误导致库失效
举例:产生雪崩的原因之一,以淘宝双十一抢购为例,假设淘宝后台将热门商品放入redis/redis-cluster(像淘宝这么大的肯定是redis-cluster redis集群喽),设置缓存时间为一小时(当然淘宝系统不会如此愚蠢,这里是假设,皮),那么午夜12点开始抢购,到了午夜1点,所有的热门商品的缓存都过期了,如果用户再购买商品,后台就是读写数据库(而不是直接读写redis)了,这样造成访问速度慢,带来无法容忍的用户体验。如图:
这样的缓存集体过期就是缓存雪崩,是使用缓存的一种危险,开发者一定要记住。
解决:
思考方式一:如果发生了集体缓存过期,即缓存雪崩,是非常可怕且很难挽救的,在发生后的一段时间内相当于没有使用redis缓存技术,退化为原始的持久层数据库操作。所以,我们的思考不是发生缓存雪崩之后如何解决,而是如何避免缓存雪崩的发生。
解决方案一——过期错开:将key的过期时间后面加上一个随机数,让key均匀的失效;或者使用一种特定的算法,使过期时间赋值更符合实际业务。
思考方式二:第一种方案均摊过期时间或使用特定算法,旨在最大程度在避免出现缓存雪崩,但是如果缓存雪崩确实发生了,程序如何应对呢?
解决方案二——排队处理:使用优先队列或者锁让程序执行在压力范围之内,如果访问量达到阈值,排队处理业务请求,即为了保证系统的不会崩溃,不要同时处理所有请求。
2.3.3 缓存击穿,iphoneX上市了
从名称上来解释含义:传统意义上的击穿(电压击穿)是指在电场作用下绝缘体内部产生破坏性的放电,绝缘电阻下降,电流增大,并产生破坏和穿孔的现象。
这里的缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存(类似电压穿破绝缘体),直接请求数据库,就像在一个屏障上凿开了一个洞。
缓存穿透与缓存击穿异同:
相同点:都是数据库承受不了巨大压力,导致崩溃。
不同点:
缓存穿透是指redis没作用,形同虚设,每一次访问都要查询数据库,导致崩溃,这是技术上可以解决的,redis中记录一个(x,null)键值对;
缓存击穿是指redis作用了,但是数据量实在是太大了,实在是承受不了这么大的数据量,数据库连带redis缓存一起崩溃,这时在固定的硬件成本下,缓存、数据库软件方面已经达到理论上的最优了,技术上解决不了。
举例:以iphoneX发布为例,一下子就成了热款,所有人通过淘宝线上购买,巨大的并发量某一时刻击穿缓存,直接请求数据库,而数据库又无法高速查表,导致系统崩溃。如图:
解决方案
思考:现在的问题是数据量实在太大了,redis和数据库的设计已经达到最优了。
步骤一:热卖商品redis有效期设置为永久,绝对不要出现过期问题,redis方面达到最优。
步骤二:在固定的硬件成本下,数据库(mysql或oracle)在表设计达到最优,框架(如mybatis)sql查询语句设计达到最优
步骤三:设置一个优先队列(如12306 买春运往返票),控制并发数,防止系统崩溃。
实际上,其实,大多数情况下这种爆款很难对数据库服务器造成压垮性的压力,能达到这种并发的可能也只有“12306春运购票”、“淘宝双十一” 、“春晚跨年”这样的事情了,从另外一个方面来讲,如果真的有某个单一商品销售量达到使用让redis、数据库崩溃,公司钱也赚了不少了,赶紧偷着笑吧!
三、redis五种基本类型的底层结构与应用
3.1 redisObject对象(类型type+编码encoding)和sds(free+len+buf)
这个很重要,要看懂后面五个类型的底层结构,要先搞懂redisObjet和sds(sdshdr)的结构
3.1.1 redisObject对象
Redis基于以上的数据结构创建了一个对象体系,包含了字符串对象,列表对象,哈希对象,集合对象,有序集合对象这五种对象.
Redis的对象体系还实现了基于引用计数技术的内存回收机制,同时基于引用计数技术实现了对象共享机制,在适当条件,通过多个数据库键共享同一个对象来节约内存.
Redis中的每一个对象都由一个redisObject结构表示,这个redisObject对象结构中和保存数据有关的三个属性:type属性、encoding属性、ptr属性,如下:
typedef struct redisObject {
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
// ...
} robj;
下面分别对类型type、编码encoding、指针ptr分别介绍:
3.1.2 类型type
类型常量 | 对象名称 | TYPE命令输出 |
REDIS_STRING | 字符串对象string | “string” |
REDIS_LIST | 列表对象list | “list” |
REDIS_HASH | 哈希对象hash(map) | “hash” |
REDIS_SET | 集合对象set | “set” |
REDIS_ZSET | 有序集合对象sorted-set | “zset” |
注意,看这个表,一定要区分好“类型常量”、“对象名称”,如下:
1)当我们称呼一个数据库键为“字符串键”时,我们指的是“这个数据库键对应的值为字符串对象”;当我们称呼一个数据库键为“列表键”时,我们指的是“这个数据库键对应的值为列表对象”;
2)TYPE命令输出(上表第三列):当我们对一个数据库键执行TYPE命令时,命令返回的结果是数据库键对应的值对象的类型,而不是键对象的类型。
其实,这些东西都是一些概念理论上的纠结,实际开发中,我们以实现需求为主,也不一定要区分的这么清楚,当然面试中可能用得到。
3.1.3 编码encoding
编码常量 | 编码对应的底层数据结构 | redis中具体类型(5种) | OBJECT ENCODING 命令输出 |
REDIS_ENCODING_INT | long类型整数(编码常量后缀是INT,但是其实现的底层数据结构是long) | REDIS_STRING | “int” |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串(SDS simple dynamic string) | REDIS_STRING | “embstr” |
REDIS_ENCODING_RAW | raw编码的简单动态字符串(SDS simple dynamic string) | REDIS_STRING | “raw” |
REDIS_ENCODING_HT | 字典(编码常量后缀为HT,表示dictionary/hashtable,即字典) | REDIS_HASH、REDIS_SET | “hashtable” |
REDIS_ENCODING_LINKEDLIST | 双向链表/双端链表(linkedlist,见名达意,不解释) | REDIS_LIST | “linkedlist” |
REDIS_ENCODING_ZIPLIST | 压缩列表(ziplist,见名达意,不解释) | REDIS_LIST、REDIS_HASH、REDIS_ZSET | “ziplist” |
REDIS_ENCODING_INTSET | 整型集合(intset,见名达意,不解释) | REDIS_SET | “intset” |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典(skiplist,见名达意,不解释) | REDIS_ZSET | “skiplist” |
3.1.4 sds(这个很重要,下面会用到)
sds英文全称 simple dynamic string,这里是简单动态字符串
struct sdshdr{
// 记录buf数组中未使用字节的数量
int free;
// 记录buf数组中已使用字节的数量,等于sds所保存字符串的长度
int len;
// 字节数组,用于保存字符串
char buff[];
}
结构(下面介绍五种基本类型底层结构会用到):
free:0 表示这个sds没有分配任何未使用空间;
len:5 表示这个sds保存了一个5个字节的字符串;
buf:Hello 表示一个char类型的数组,数组的前五个字节分别保存了‘H’‘e’‘l’‘l’‘o’,最后一个字符保存了空字符‘\0’.
3.2 字符串对象string
由上面的编码表可以知道,字符串编码包括三种 int raw embstr,三者对比:
如果保存的是整数值,且可用long类型表示,那么编码设为int;
如果保存的是一个字符串,并且长度大于32字节,那么使用SDS(simple dynamic string,简单动态字符串)保存,编码设为raw;
如果保存的是一个字符串,并且长度小于或等于32字节,那么编码设为embstr;
3.2.1 int编码
对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为string
encoding,redis任何一种基本类型至少有两种编码,这里是int
ptr:指针,指向底层数据结构的指针,这里指向底层long类型数据7758258
3.2.2 raw编码
对于这个图的解释:
redisObject: 分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为string
encoding,redis任何一种基本类型至少有两种编码,这里是raw
ptr:指针,指向底层数据结构的指针,这里指向sdshdr
sdshdr :
sds simple dynamic string,简单动态字符串 ; hdr High available Data Replication,高可用性复制 ; 合在一起 sds hdr 简单动态字符串高可用性复制,包括三个 free 表示
free:0 表示这个sds没有分配任何未使用空间;
len:36表示这个sds保存了一个36个字节的字符串;
buf:Hello 表示一个char类型的数组,数组的前36个字节分别保存了’H’‘e’‘l’‘l’‘o’‘ ’‘w’‘o’‘r’‘l’‘d’'H’‘e’‘l’‘l’‘o’‘ ’‘w’‘o’‘r’‘l’‘d’‘H’‘e’‘l’‘l’‘o’‘ ’‘w’‘o’‘r’‘l’‘d’’.’‘.’‘.’,
最后一个字符保存了空字符‘\0’.
3.2.3 embstr编码
对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为string
encoding,redis任何一种基本类型至少有两种编码,这里是embstr
ptr:指针,指向底层数据结构的指针,这里指向sdshdr
sdshdr :
sds simple dynamic string,简单动态字符串
hdr High available Data Replication,高可用性复制
合在一起 sds hdr 简单动态字符串高可用性复制,包括三个 free 表示
free:0 表示这个sds没有分配任何未使用空间;
len:5 表示这个sds保存了一个5个字节的字符串;
buf:Hello 表示一个char类型的数组,数组的前五个字节分别保存了‘H’‘e’‘l’‘l’‘o’,最后一个字符保存了空字符‘\0’.
最后点一下,用long double类型表示的浮点数在redis中也是字符串来表示的,了解即可。
编码是指存储类型的方式,字符串对象中embstr编码和raw编码有什么区别?
当字符串长度小于32字节,字符串对象将使用emstr编码,大于32字节,字符串使用raw。
embstr直接一次性创建一块内存,内存一定是连续的;raw会分别两次创建redisObject结构与sdshdr结构,内存不一定是连续的。
embstr优势:
(1) 内存释放更快:内存释放是embstr只需要释放一次,而raw需要释放两次
(2) 查找更快: embstr查找的更快
3.3 列表对象list
由上面的编码表可以知道,字符串编码包括两种:linkedlist ziplist
3.3.1 ziplist编码
zlbytes:表示的是总长度,总字节数
zllen:表示的是数据部分的长度
两个不一样的。
3.3.2 linkedlist编码
对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为list
encoding,redis任何一种基本类型至少有两种编码,这里是linkedlist
ptr:指针,指向底层数据结构的指针
1 表示ziplist第一个元素
“three” 表示ziplist第二个元素
5 表示ziplist第三个元素
list两种编码方式(ziplist和linkedlist区别)
选用:当列表对象保存的所有字符串元素的长度都小于64字节,并且列表对象保存的元素数量小于512时,list使用ziplist编码;不能满足这两种情况就是用linkedlist编码。
优缺点:ziplist的特点是节省内存,但是插入速度慢;linkedlist是一个双向列表,特点就是插入速度快,但是占内存。
3.4 哈希对象hash(map)
由上面的编码表可以知道,字符串编码包括两种:ziplist hashtable
3.4.1 ziplist编码
对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为list
encoding,redis任何一种基本类型至少有两种编码,这里是ziplist
ptr:指针,指向底层数据结构的指针
zlbytes: 表示整个ziplist的字节数(总长度)
zltail: 表示整个ziplist的头部
zllen: 表示整个ziplist 数据部分的长度
(key,value)=(name,Tom) 表示ziplist第一个元素
(key,value)=(age,25) 表示ziplist第二个元素
(key,value)=(career,Programmer) 表示ziplist第三个元素
zlled 表示整个ziplist的尾部
3.4.2 hashtable编码
对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为list
encoding,redis任何一种基本类型至少有两种编码,这里是hashtable
ptr:指针,指向底层数据结构的指针
hash对象的ziplist编码和hashtable编码
当一个哈希对象可以满足以下两个条件中的任意一个,哈希对象会选择使用ziplist编码来进行存储:1、哈希对象中的所有键值对总长度(包括键和值)小于64字节(这个阈值可以通过参数hash-max-ziplist-value 来进行控制)。
2、哈希对象中的键值对数量小于512个(这个阈值可以通过参数hash-max-ziplist-entries 来进行控制)。
一旦不满足这两个条件中的任意一个,哈希对象就会选择使用hashtable来存储。
3.5 集合对象set
由上面的编码表可以知道,字符串编码包括两种:intset hashtable
3.5.1 intset编码
对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为set
encoding,redis任何一种基本类型至少有两种编码,这里是intset
ptr:指针,指向底层数据结构的指针
set对象inset和hashtable编码转换:
当Set对象可以同时满足以下两个条件时, 对象使用 intset 编码:
1.Set对象保存的所有元素都是整数值;
2.Set对象保存的元素数量不超过 512 个;
不能满足这两个条件的Set对象需要使用 hashtable 编码。
3.5.2 hashtable编码
对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为set
encoding,redis任何一种基本类型至少有两种编码,这里是hashtable
ptr:指针,指向底层数据结构的指针
3.6 有序集合对象sortedset
有序集合是list和set的一种居中,如下:
由上面的编码表可以知道,字符串编码包括两种:ziplist skiplist
3.6.1 ziplist编码
ziplist编码的有序集合对象使用压缩列表作为底层实现。每个集合使用2个紧挨在一起的压缩列表节点来保存,第一个保存元素的成员,第二个保存元素的分值。压缩列表内的集合按分值从小到大排序,分值较小的元素被放置在靠近表头的位置,分值较大的元素在靠近表尾的位置。
对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为sorted set
encoding,redis任何一种基本类型至少有两种编码,这里是ziplist
ptr:指针,指向底层数据结构的指针
zlbytes: 表示整个ziplist的字节数(总长度)
zltail: 表示整个ziplist的头部
zllen: 表示整个ziplist 数据部分的长度
(key,value)=(apple,8.5) 表示ziplist第一个元素
(key,value)=(banana,5.0) 表示ziplist第二个元素
(key,value)=(cherry,6.0) 表示ziplist第三个元素
zlled 表示整个ziplist的尾部
3.6.2 skiplist编码
skiplist编码的有序集合对象使用 zset结构作为底层实现,zset结构同时包含一个字典和一个跳跃表。如下:
typedef struct zset{
dict *dict; // 字典dict
zskiplist *zsl; // 跳跃表zsl
}zset; //zset是有序集合,同时由字典dict和跳跃表zsl实现
为什么有序集合zset(sorted set)要同时由字典dict和跳跃表实现?
跳跃表利于执行范围操作(跳跃表是排好序的),而字典有利于执行分值查找操作。同时由于Redis里的跳跃表和字典元素很多都是用指针实现的,所以不会浪费内存。
对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为sorted set
encoding,redis任何一种基本类型至少有两种编码,这里是skiplist
ptr:指针,指向底层数据结构的指针
zset 表示sorted set 实体
dict 表示字典
zsl 表示sorted skiplist 跳跃表
…… 表示跳跃
(key,value)=(apple,8.5) 表示ziplist第一个元素
(key,value)=(banana,5.0) 表示ziplist第二个元素
(key,value)=(cherry,6.0) 表示ziplist第三个元素
有序集合zset两个编码类型
问题:ziplist数据结构是如何保证zset有序的?
回答:通过value值,ziplist要保证集合中的数据有序,会将key放在前面一位,然后将key所对应value放在key的后一位,这样就能够保证集合的有序。
问题:skiplist数据结构是如何保证zset有序的?
回答:跳跃链表本来就是有序的,直接使用即可
问题:ziplist和skiplist的编码转换?
回答: 当有序集合对象同时满足以下两个条件时,对象使用压缩链表编码:
(1) 有序集合保存的元素数量小于128个;
(2) 有序集合保存的所有元素成员的长度都小于64字节;
如果不满足上述两个条件,那么ZipList会转化为SkipList,同时,当后面的SkipList的元素数量和元素成员的长度满足要求时,也不会回退为ZipList。
3.7 五种基本类型的特点与应用
3.7.1 String数据类型的特点与应用
String的特点:最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。
String的应用
第一,缓存功能,数据库缓存用redis的string类型实现
因为String字符串是各种语言都支持的、最常用的数据类型,不仅仅是Redis;因此,在redis连接各种语言的时候,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
金手指:redis中String作为缓存,也是String的最常用的,redis最常用的。
第二,计数器,计数用redis的string类型实现
许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
金手指:redis持久化包括AOF热备份和RDB冷备份。
第三,共享用户Session,共享用户session放到redis的String中存储
用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成,大大提高效率。
注意:真实的开发环境中,很多人可能会把很多比较复杂的结构也统一转成String去存储使用,比如有的人就喜欢把对象或者List转换为JSONString进行存储,拿出来再反序列化,但是啥都是用的String不够优雅。但是,总原则还是:在最合适的场景使用最合适的数据结构,对象找不到最合适的但是类型可以选最合适的。
3.7.2 Hash数据类型的特点与应用
Hash的特点
这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。
Hash的应用
hash的应用场景很少,因为现在很多对象都是比较复杂的,比如你的商品对象可能里面就包含了很多属性,其中也有对象,单一hash无法满足。
3.7.3 List数据类型的特点与应用
List特点
有序列表,有序,元素可以重复
List应用
(1)列表型数据item-detail:比如可以通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。比如,对于csdn博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表。
(2)分页数据存储:比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询,这个是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。比如:对于csdn博客网站,当文章多时,item需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能,大大提高查询效率。
(3)消息队列:比如可以搞个简单的消息队列,从 List 头怼进去,从 List 屁股那里弄出来。Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
3.7.4 Set数据类型的特点与应用
Set特点:无序集合,不保证顺序但是提供自动去重的功能,我们最重要的是使用他这个去重功能,下面两个实例都是。
Set应用:
(1)分布式多服务器全局数据去重:直接基于 Set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进行全局的 Set 去重。
(2)集合的交并补运算:可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,可以得到俩人的共同好友是谁,比如qq中你和一个人有多少个共同好友。
3.7.5 SortedSet数据类型的特点与应用
SortedSet的特点: 是排序的 Set,去重但可以排序,我们最重要的使用它这个自定义排序的功能。
SortedSet的应用:
(1) 当前item排序:写进去的时候给一个分数,自动根据分数排序:有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。
(2) 各种排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单排序依据可能是多方面:按照时间、按照播放量、按照获得的赞数等。微博热搜榜,就是有个后面的热度值,前面就是名称。为什么不用list来做排行榜?list无法自定义顺序比较,无法保证无重复元素。
(3) 权重排序:用Sorted Sets来做带权重的队列:比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
sortedSet和list都排序,两者区别:
(1) 特点不同:sortedSet元素自动去重,list无法完成;sortedSet自定义排序器,list无法完成。
(2) 应用不同:sortedSet用于对当前item排序,排行榜,权重排序,都是一样的;list用于制作item,lrange制作item分页,消息队列FIFO.
小结:Redis基础数据类型有五种,总原则就是:在最合适的场景使用最合适的数据结构,不要什么都用string去实现。一般来说,一个好的面试题,是对于不同层级的人可以给出不同深度的答案,就比如redis五种基本类型,一定要从底层结构图和项目实践在什么地方使用这个数据类型两方面来说,同时兼顾底层和项目。
四、Redis七种特殊数据类型
4.1 位图Bitmap
4.1.1 位图的特点
位图可以用最小的空间存放最大量的数据,比如,对于bool类型,按位存放,用 0|1 来表示 false|true,由于一个字节8位,相对于String类型用一个字节来存放 true|false ,使用位图的方式,空间缩小为原来的 1/8 。
值得注意的是,位图不是一个独立的数据结构,从底层来说,它是string字符串类型。
位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 将 byte 数组看成位数组来处理。
4.1.2 位图的应用
位图的最常见的应用就是用位来存储bool类型的true|false,比如统计签到和统计日活/月活。
4.1.2.1 统计签到
业务情景1:在我们平时开发过程中,会有一些 bool 型数据需要存取(即只有两个值 true|false 0|1),比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。
解决:为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。
具体实现:
key 可以设置为 “前缀:用户id:年月” ,譬如 setbit sign:123:1909 0 1
代表用户ID=123签到,签到的时间是19年9月份,0代表该月第一天,1代表签到了。第二天没有签到,无需处理,系统默认为0,第三天签到 setbit sign:123:1909 2 1
,可以查看一下目前的签到情况,显示第一天和第三天签到了,前8天目前共签到了2天,如下:
127.0.0.1:6379> setbit sign:123:1909 0 1 set 第一天签到了
0
127.0.0.1:6379> setbit sign:123:1909 2 1 set 第三天签到了
0
127.0.0.1:6379> getbit sign:123:1909 0 get 查看第一天是否签到,返回为1,第一天签到了
1
127.0.0.1:6379> getbit sign:123:1909 1 get 查看第二天是否签到,返回为0,第二天没签到
0
127.0.0.1:6379> getbit sign:123:1909 2 get 查看第三天是否签到,返回为1,第三天签到了
1
127.0.0.1:6379> getbit sign:123:1909 3 get查看第四天是否前端,返回为0,第四天没签到
0
127.0.0.1:6379> bitcount sign:123:1909 0 0 count查看所有签到天数,返回为2,一共两天签到
2
4.3.1.2 统计日活/月活
业务情景:当我们要统计日活/月活的时候,因为需要去重,需要使用 set 来记录所有活跃用户的 id,这非常浪费内存。
解决:可以看作是存储bool类型数据问题,所以可以考虑使用位图来标记用户的活跃状态。每个用户会都在这个位图的一个确定位置上,0 表示不活跃,1 表示活跃。然后到第二天/月底遍历一次位图就可以得到日度活跃用户数/月度活跃用户数。
4.2 HyperLogLog
从统计页面PV到统计页面UV,从set数据类型到HyperLogLog数据类型
4.2.1 HyperLogLog的特点
HyperLogLog数据结构,添加操作命令为pfadd,查看数量命令为pfcount,两个命令中这个 pf 是HyperLogLog 这个数据结构的发明人 Philippe Flajolet 的首字母缩写。
HyperLogLog与上面使用的位图bitmap一样,在完成同一业务下,使用更小的空间存储数据(相对于set数据结构)。其核心应用在于计数,常见的业务是统计网页的PV和UV。
4.2.2 HyperLogLog的应用
业务情景:统计网页的PV和UV
如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。但是 UV 不一样,它要去重,即同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。
当前问题:PV直接统计数量就好了,UV要在统计PV数量上根据用户id去重
注意1:无论是PV,还是UV,都不需要特别精准的数据,一个大致数据就好了
注意2:无论是PV,还是UV,都是针对页面来说的,页面的PV,页面的UV
对于统计UV,这里提供两种方案,Set数据类型和HyperLogLog数据类型
方案一:使用set数据类型
理由:set数据结构自带去重,只要value是用户id,会自动去重
用法与优点:使用set数据结构,为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用户 ID。当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。
缺点:第一,爆款页面:如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。
第二,多个页面:如果页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解决方案呢?
方案二:使用HyperLogLog数据类型
HyperLogLog的两个命令:HyperLogLog 提供了两个指令 pfadd 和 pfcount,顾名思义,一个是增加计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是;pfcount 和 scard 用法是一样的,直接获取计数值。
具体实现(HyperLogLog数据类型统计UV)
127.0.0.1:6379> pfadd codehole user1 // 对于HyperLogLog类型变量codehole,添加变量user1
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为1
(integer) 1
127.0.0.1:6379> pfadd codehole user2 // 对于HyperLogLog类型变量codehole,添加变量user2
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为2
(integer) 2
127.0.0.1:6379> pfadd codehole user3 // 对于HyperLogLog类型变量codehole,添加变量user3
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为3
(integer) 3
127.0.0.1:6379> pfadd codehole user4 // 对于HyperLogLog类型变量codehole,添加变量user4
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为4
(integer) 4
127.0.0.1:6379> pfadd codehole user5 // 对于HyperLogLog类型变量codehole,添加变量user5
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为5
(integer) 5
127.0.0.1:6379> pfadd codehole user6 // 对于HyperLogLog类型变量codehole,添加变量user6
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为6
(integer) 6
127.0.0.1:6379> pfadd codehole user7 user8 user9 user10 // 对于HyperLogLog类型变量codehole,添加变量user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为10
(integer) 10
4.3 布隆过滤器
4.3.1 布隆过滤器的特点
HyperLogLog局限与布隆过滤器的引入
HyperLogLog局限:对于大数据量,HyperLogLog 数据结构来进行估数,它非常有价值,可以解决很多精确度不高的统计需求。但是如果我们想知道某一个值是不是已经在 HyperLogLog 结构里面了,它就无能为力了,它只提供了 pfadd 和 pfcount 方法,没有提供 pfcontains 这种方法。即对于大数据量,HyperLogLog只能添加和计数,无法判断是否存在contains/exists,这个时候就需要布隆过滤器了。
业务场景:我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?
方案一:在服务端记录用户看过的所有历史记录
因为新闻是大数据量,所以使用 HyperLogLog 数据结构,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录,如下:
127.0.0.1:6379> bf.add codehole user1 // bf.add 添加 user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2 // bf.add 添加 user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3 // bf.add 添加 user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user1 // bf.exists user1,返回为1,存在
(integer) 1
127.0.0.1:6379> bf.exists codehole user2 // bf.exists user2,返回为1,存在
(integer) 1
127.0.0.1:6379> bf.exists codehole user3 // bf.exists user3,返回为1,存在
(integer) 1
127.0.0.1:6379> bf.exists codehole user4 // bf.exists user4 返回为0,不存在
(integer) 0
127.0.0.1:6379> bf.madd codehole user4 user5 user6 // bf.madd 添加user4 user5 user6
(1) (integer) 1
(2) (integer) 1
(3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7 // bf.exists user4 user5 user6 user7 前面三个返回1,存在,user7返回为0,不存在
(1) (integer) 1
(2) (integer) 1
(3) (integer) 1
(4) (integer) 0
问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上无法满足。
方案二:使用布隆过滤器,判断是否存在 contains|exists
第一,布隆过滤器的initial_size设置:布隆过滤器的initial_size估计的过大,会浪费存储空间;布隆过滤器的initial_size估计的过小,就会影响准确率。用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出估计值很多。
第二,布隆过滤器的error_rate设置:布隆过滤器的error_rate越小,需要的存储空间就越大,对于不需要过于精确的场合,error_rate设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变。
4.3.2 布隆过滤器的应用
布隆过滤器的核心在于快速判断是否存在,去重功能,一般有一下三个应用:
1、爬虫去重,爬虫快速判断是否存在,对于已经存在的去重:在爬虫系统中,我们需要对 URL 进行去重,已经爬过的网页就可以不用爬了。但是 URL 太多了,几千万几个亿,如果用一个集合装下这些 URL 地址那是非常浪费空间的。这时候就可以考虑使用布隆过滤器。它可以大幅降低去重存储消耗,只不过也会使得爬虫系统错过少量的页面。
2、NoSQL数据库,不在数据库的row不到数据库磁盘中去找:布隆过滤器在 NoSQL 数据库领域使用非常广泛,我们平时用到的 HBase、Cassandra 还有 LevelDB、RocksDB 内部都有布隆过滤器结构,布隆过滤器可以显著降低数据库的 IO 请求数量。当用户来查询某个 row 时,可以先通过内存中的布隆过滤器过滤掉大量不存在的 row 请求,然后再去磁盘进行查询。
3、垃圾邮件过滤,设置判断算法,如果是满足判断算法,算做垃圾邮件:邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平时也会遇到某些正常的邮件被放进了垃圾邮件目录中,这个就是误判所致,概率很低。
4.4 Geospatial
作用:用来保存地理位置,并作位置距离计算或者根据半径计算位置等。即可以用Redis来实现附近的人或者计算最优地图路径。
业务实际:查找附近的人,使用GeoHash数据类型,使用布隆过滤器去重
4.5 Pub/Sub
作用:功能是订阅发布功能,可以用作简单的消息队列。
4.6 Pipeline
作用:可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
Pipeline可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。
4.7 Lua脚本
Redis 支持提交 Lua 脚本来执行一系列的功能。电商项目中,秒杀场景经常使用Lua脚本,利用他的原子性。
4.8 Redis事务
最后一个功能是事务,但 Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。
4.9 小结
第一,Bitmap位图
原理:支持按 bit 位来存储信息
作用:用来实现 布隆过滤器(BloomFilter);
第二,HyperLogLog
作用:供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV,相对于Set数据结构有大数据量处理的优势,缺点是不精确。
第三,布隆过滤器
作用:在大数据处理时可以判断某个数据存不存在。都是处理大数据量,布隆过滤器主要用来判断,某个数据存不存在,HyperLogLog用来做统计 , 没法确认某个数据在不在。
第四,Geospatial
作用:用来保存地理位置,并作位置距离计算或者根据半径计算位置等。即可以用Redis来实现附近的人或者计算最优地图路径。
第五,pub/sub
作用:功能是订阅发布功能,可以用作简单的消息队列。
第六,Pipeline
作用:可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
第七,Lua脚本
Redis 支持提交 Lua 脚本来执行一系列的功能。电商项目中,秒杀场景经常使用Lua脚本,利用他的原子性。
高并发博客中“电商项目的秒杀设计”中讲到,库存预热带来的问题(解释:现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的),使用redis中的Lua脚本的原子性处理。
第八,事务
最后一个功能是事务,但 Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。
五、单机版redis及Java开发实践
5.1 单机版Redis安装
步骤一:安装相关依赖
yum install gcc-c++
步骤二:官网下载redis源码包、解压
官网下载redis源码包
解压:tar -zxvf redis-3.0.0.tar.gz
步骤三:编译和安装
解压后,进入解压目录,编译安装
[root@bogon redis-3.0.0]# make
[root@bogon redis-3.0.0]# make install PREFIX=/usr/local/redis (要安装的目录 PREFIX一定要大写)
步骤四:启动(进入刚刚安装位置启动)
cd /usr/local/redis/bin
./redis-server (启动)
成功标志:
看到这个图片为启动成功。
附:redis两种启动方式 (前端启动+后端启动)
1、默认启动方式为前端启动
cd /usr/local/redis/bin/ (这里指进入redis安装目录,bin目录下)
./redis-server (启动)
默认是前端启动模式,端口是6379
2、后端启动
(1)从redis的解压目录中复制redis.conf到redis的安装目录(本文中/usr/local/redis)
(2)修改安装目录下刚刚复制过来的配置文件:
daemonize yes (表示使用后端模式启动)
(3)[root@bogon bin]# ./redis-server redis.conf 启动
单机版redis安装完成。
步骤五:测试使用(centos上测试使用)
127.0.0.1:6379> set a 10
OK
127.0.0.1:6379> get a
"10"
可以看到,redis可以正常使用。
5.2 Java项目中使用redis
5.2.1 Junit测试使用Jedis单连接redis
导入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.0</version>
</dependency>
test使用
// Junit测试连接redis和基本的set-get操作
@Test
public void testJedisSingle() {
Jedis jedis = new Jedis("192.168.101.3", 6379);
//这里表示centos IP为192.168.101.3
jedis.set("name", "bar");
String name = jedis.get("name");
System.out.println(name);
jedis.close();
}
附:如果centos上的redis正常启动而且测试可以完成set-get基本操作,但是本地无法连接上centos上的redis,查看防火墙是否关闭。正确的操作是关闭防火墙或开启6379号端口。centos7.0防火墙操作如下:
关闭防火墙
systemctl status firewalld.service查看防火墙状态
systemctl stop firewalld.service 关闭防火墙
systemctl status firewalld.service查看防火墙状态
开启6379端口
添加
firewall-cmd --zone=public --add-port=6379/tcp --permanent (--permanent永久生效,没有此参数重启后失效)
重新载入
firewall-cmd --reload
查看
firewall-cmd --zone= public --query-port=6379/tcp
注:关闭防火墙和打开6379号端口只要两者选一即可。
5.2.2 进阶——Junit测试使用连接池连接redis
通过单实例连接redis不能对redis连接进行共享,可以使用连接池对redis连接进行共享,提高资源利用率,使用jedisPool连接redis服务,如下代码:
@Test
public void pool() {
JedisPoolConfig config = new JedisPoolConfig();
//最大连接数
config.setMaxTotal(30);
//最大连接空闲数
config.setMaxIdle(2);
JedisPool pool = new JedisPool(config, "192.168.101.3", 6379);
Jedis jedis = null;
try {
jedis = pool.getResource();
jedis.set("name", "lisi");
String name = jedis.get("name");
System.out.println(name);
}catch(Exception ex){
ex.printStackTrace();
}finally{
if(jedis != null){
//关闭连接
jedis.close();
}
}
}
5.2.3 再次进阶——在spring中使用redis
上面的都是使用Junit的测试代码,不是项目中真正运行有效代码,这里介绍spring中整合redis
配置spring配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.2.xsd ">
<!-- 连接池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大连接数 -->
<property name="maxTotal" value="30" />
<!-- 最大空闲连接数 -->
<property name="maxIdle" value="10" />
<!-- 每次释放连接的最大数目 -->
<property name="numTestsPerEvictionRun" value="1024" />
<!-- 释放连接的扫描间隔(毫秒) -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 连接最小空闲时间 -->
<property name="minEvictableIdleTimeMillis" value="1800000" />
<!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 -->
<property name="softMinEvictableIdleTimeMillis" value="10000" />
<!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->
<property name="maxWaitMillis" value="1500" />
<!-- 在获取连接的时候检查有效性, 默认false -->
<property name="testOnBorrow" value="true" />
<!-- 在空闲时检查有效性, 默认false -->
<property name="testWhileIdle" value="true" />
<!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
<property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis单机 通过连接池 -->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="close">
<constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
<constructor-arg name="host" value="192.168.101.3"/>
<constructor-arg name="port" value="6379"/>
</bean>
测试代码:
private ApplicationContext applicationContext;
@Before
public void init() {
applicationContext = new ClassPathXmlApplicationContext(
"classpath:applicationContext.xml");
}
@Test
public void testJedisPool() {
JedisPool pool = (JedisPool) applicationContext.getBean("jedisPool");
try {
jedis = pool.getResource();
jedis.set("name", "lisi");
String name = jedis.get("name");
System.out.println(name);
}catch(Exception ex){
ex.printStackTrace();
}finally{
if(jedis != null){
//关闭连接
jedis.close();
}
}
}
5.4 小结(redis单机版)
redis单机版使用起来的并不难,下载对应版本,在centos上安装好,设置好防火墙和端口,即可使用jedis连接使用。
六、集群版redis及Java开发实践
6.1 redis集群原理
6.1.1 Redis集群架构图
redis集群架构细节:
(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
(2)节点的fail是通过集群中超过半数的节点检测失效时才生效.
(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
6.1.2 redis-cluster投票:容错
redis集群的容错机制:投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超过(cluster-node-timeout),认为当前master节点挂掉.什么时候整个集群不可用(cluster_state:fail)?
(1) 如果集群任意master挂掉,且当前master没有slave.集群进入fail状态,也可以理解成集群的slot映射[0-16383]不完成时进入fail状态. ps : redis-3.0.0.rc1加入cluster-require-full-coverage参数,默认关闭,打开集群兼容部分失败.
(2) 如果集群超过半数以上master挂掉,无论是否有slave集群进入fail状态.
当集群不可用时,所有对集群的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)错误。
6.2 集群版redis(redis-cluster)安装
6.2.1 手把手搭建ruby环境
redis集群管理工具redis-trib.rb依赖ruby环境,故首先需要在centos上安装ruby环境:
安装ruby环境:
yum install ruby
yum install rubygems
安装好ruby环境后,安装ruby和redis的接口程序:
拷贝redis-3.0.0.gem至/usr/local下
执行:
gem install /usr/local/redis-3.0.0.gem
6.2.2 新建集群所需结点(6个)
这里在同一台服务器用不同的端口表示不同的redis服务器,如下:
主节点:192.168.101.3:7001 192.168.101.3:7002 192.168.101.3:7003
从节点:192.168.101.3:7004 192.168.101.3:7005 192.168.101.3:7006
在/usr/local下创建redis-cluster目录,其下创建7001、7002。。7006目录,命令如下:
cd /usr/local (进入/usr/local目录)
mkdir redis-cluster (创建redis-cluster目录)
cd redis-cluster (进入redis-cluster目录)
mkdir 7001 7002 7003 7004 7005 7006 (创建7001 7002 7003 7004 7005 7006目录)
运行结果如下:
将redis安装目录bin下的文件拷贝到每个700X(一共7001~7006 6个)目录内,同时将redis源码目录src下的redis-trib.rb拷贝到redis-cluster目录下。
修改每个700X目录下的redis.conf配置文件:
port XXXX (指定redis端口,但是记住,6个redis的端口不同,避免端口冲突)
#bind 192.168.101.3
cluster-enabled yes (允许构成集群)
6.2.3 启动所有redis结点(6个)
这里使用后端启动,分别进入7001、7002、…7006目录,执行:
./redis-server ./redis.conf
然后查看6个是否都已经成功启动:
ps aux|grep redis
运行结果,如下则为6个都启动成功(7001~7006)(PS:ps aux|grep 进程名,查看进程启动情况,可以在任意目录下执行此句,不一定要在7006目录下)
6.2.4 将6个redis结点集成
执行redis-trib.rb,此脚本是ruby脚本,它依赖ruby环境。
./redis-trib.rb create --replicas 1 192.168.101.3:7001 192.168.101.3:7002 192.168.101.3:7003 192.168.101.3:7004 192.168.101.3:7005 192.168.101.3:7006
(嗯,你没有想错,6个redis创建一个redis集群只需要这仅仅一条命令)
输出(创建集群输出)如下:
>>> Creating cluster
Connecting to node 192.168.101.3:7001: OK
Connecting to node 192.168.101.3:7002: OK
Connecting to node 192.168.101.3:7003: OK
Connecting to node 192.168.101.3:7004: OK
Connecting to node 192.168.101.3:7005: OK
Connecting to node 192.168.101.3:7006: OK
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
192.168.101.3:7001
192.168.101.3:7002
192.168.101.3:7003
Adding replica 192.168.101.3:7004 to 192.168.101.3:7001
Adding replica 192.168.101.3:7005 to 192.168.101.3:7002
Adding replica 192.168.101.3:7006 to 192.168.101.3:7003
M: cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 192.168.101.3:7001
slots:0-5460 (5461 slots) master
M: 4e7c2b02f0c4f4cfe306d6ad13e0cfee90bf5841 192.168.101.3:7002
slots:5461-10922 (5462 slots) master
M: 1a8420896c3ff60b70c716e8480de8e50749ee65 192.168.101.3:7003
slots:10923-16383 (5461 slots) master
S: 69d94b4963fd94f315fba2b9f12fae1278184fe8 192.168.101.3:7004
replicates cad9f7413ec6842c971dbcc2c48b4ca959eb5db4
S: d2421a820cc23e17a01b597866fd0f750b698ac5 192.168.101.3:7005
replicates 4e7c2b02f0c4f4cfe306d6ad13e0cfee90bf5841
S: 444e7bedbdfa40714ee55cd3086b8f0d5511fe54 192.168.101.3:7006
replicates 1a8420896c3ff60b70c716e8480de8e50749ee65
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join...
>>> Performing Cluster Check (using node 192.168.101.3:7001)
M: cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 192.168.101.3:7001
slots:0-5460 (5461 slots) master
M: 4e7c2b02f0c4f4cfe306d6ad13e0cfee90bf5841 192.168.101.3:7002
slots:5461-10922 (5462 slots) master
M: 1a8420896c3ff60b70c716e8480de8e50749ee65 192.168.101.3:7003
slots:10923-16383 (5461 slots) master
M: 69d94b4963fd94f315fba2b9f12fae1278184fe8 192.168.101.3:7004
slots: (0 slots) master
replicates cad9f7413ec6842c971dbcc2c48b4ca959eb5db4
M: d2421a820cc23e17a01b597866fd0f750b698ac5 192.168.101.3:7005
slots: (0 slots) master
replicates 4e7c2b02f0c4f4cfe306d6ad13e0cfee90bf5841
M: 444e7bedbdfa40714ee55cd3086b8f0d5511fe54 192.168.101.3:7006
slots: (0 slots) master
replicates 1a8420896c3ff60b70c716e8480de8e50749ee65
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
附加说明1:语句中replicas指定为1表示每个主节点有一个从节点。实际上,任何一个redis集群至少需要3个主节点,这里因为一共6个结点,拿出3个主节点后,所以3个从节点,每一个主节点有一个从节点。
附加说明2:
若执行时报如下错误:
[ERR] Node XXXXXX is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0
解决方法是删除生成的配置文件nodes.conf,如果不行则说明现在创建的结点包括了旧集群的结点信息,需要删除redis的持久化文件后再重启redis,比如:appendonly.aof、dump.rdb
6.2.5 查询集群信息(结点信息+状态信息)
步骤一:集群创建成功登陆任意redis结点查询集群中的节点情况。
客户端以集群方式登陆:./redis-cli -c -h 192.168.101.3 -p 7001
如下:
说明:
./redis-cli -c -h 192.168.101.3 -p 7001 ,其中-c表示以集群方式连接redis,-h指定ip地址,-p指定端口号
登录之后,查询结点信息和状态信息:
cluster nodes 查询集群结点信息
cluster info 查询集群状态信息
6.3 Java使用redis集群、Spring容器使用redis集群(jedisCluster)
6.3.1 Java使用Junit测试方法
// 连接redis集群
@Test
public void testJedisCluster() {
JedisPoolConfig config = new JedisPoolConfig();
// 最大连接数
config.setMaxTotal(30);
// 最大连接空闲数
config.setMaxIdle(2);
//集群结点
Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7001));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7002));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7003));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7004));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7005));
jedisClusterNode.add(new HostAndPort("192.168.101.3", 7006));
JedisCluster jc = new JedisCluster(jedisClusterNode, config);
JedisCluster jcd = new JedisCluster(jedisClusterNode);
jcd.set("name", "zhangsan");
String value = jcd.get("name");
System.out.println(value);
}
6.3.2 Spring使用redis集群
代码1——Spring配置文件applicationContext.xml配置:
<!-- 连接池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大连接数 -->
<property name="maxTotal" value="30" />
<!-- 最大空闲连接数 -->
<property name="maxIdle" value="10" />
<!-- 每次释放连接的最大数目 -->
<property name="numTestsPerEvictionRun" value="1024" />
<!-- 释放连接的扫描间隔(毫秒) -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 连接最小空闲时间 -->
<property name="minEvictableIdleTimeMillis" value="1800000" />
<!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 -->
<property name="softMinEvictableIdleTimeMillis" value="10000" />
<!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->
<property name="maxWaitMillis" value="1500" />
<!-- 在获取连接的时候检查有效性, 默认false -->
<property name="testOnBorrow" value="true" />
<!-- 在空闲时检查有效性, 默认false -->
<property name="testWhileIdle" value="true" />
<!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
<property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis集群 -->
<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
<constructor-arg index="0">
<set>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7001"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7002"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7003"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7004"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7005"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg index="0" value="192.168.101.3"></constructor-arg>
<constructor-arg index="1" value="7006"></constructor-arg>
</bean>
</set>
</constructor-arg>
<constructor-arg index="1" ref="jedisPoolConfig"></constructor-arg>
</bean>
代码2——spring容器测试redis集群
private ApplicationContext applicationContext;
@Before
public void init() {
applicationContext = new ClassPathXmlApplicationContext(
"classpath:applicationContext.xml");
}
//redis集群
@Test
public void testJedisCluster() {
JedisCluster jedisCluster = (JedisCluster) applicationContext
.getBean("jedisCluster");
jedisCluster.set("name", "zhangsan");
String value = jedisCluster.get("name");
System.out.println(value);
}
6.4 redis集群节点操作(包括:添加主节点、添加从节点、删除节点)
6.4.1 添加主节点
集群创建成功后可以向集群中添加节点,下面是添加一个master主节点
添加7007结点
执行下边命令:
./redis-trib.rb add-node 192.168.101.3:7007 192.168.101.3:7001
进入redis结点,查看集群结点发现7007已添加到集群中:
centosIP:redis端口号>cluster nodes
添加完主节点需要对主节点进行hash槽分配这样该主节才可以存储数据。
redis集群有16384个槽,集群中的每个结点分配自已槽,通过查看集群结点可以看到槽占用情况。
给刚添加的7007结点分配槽:
第一步:连接上集群
./redis-trib.rb reshard 192.168.101.3:7001(连接集群中任意一个可用结点都行)
第二步:输入要分配的槽数量
输入 500表示要分配500个槽
第三步:输入接收槽的结点id
这里准备给7007分配槽,通过cluster nodes查看7007结点id为15b809eadae88955e36bcdbb8144f61bbbaf38fb
输入:15b809eadae88955e36bcdbb8144f61bbbaf38fb
第四步:输入源结点id
这里输入all
第五步:输入yes开始移动槽到目标结点id
6.4.2 添加从节点
集群创建成功后可以向集群中添加节点,下面是添加一个slave从节点。
添加7008从结点,将7008作为7007的从结点。
./redis-trib.rb add-node --slave --master-id 主节点id 添加节点的ip和端口 集群中已存在节点ip和端口
执行如下命令:
./redis-trib.rb add-node --slave --master-id cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 192.168.101.3:7008 192.168.101.3:7001
cad9f7413ec6842c971dbcc2c48b4ca959eb5db4 是7007结点的id,可通过cluster nodes查看。
注意:如果原来该结点在集群中的配置信息已经生成cluster-config-file指定的配置文件中(如果cluster-config-file没有指定则默认为nodes.conf),这时可能会报错:
[ERR] Node XXXXXX is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0
解决方法是删除生成的配置文件nodes.conf,删除后再执行./redis-trib.rb add-node
指令
查看集群中的结点,刚添加的7008为7007的从节点:
6.4.3 删除节点
./redis-trib.rb del-node 127.0.0.1:7005 4b45eb75c8b428fbd77ab979b85080146a9bc017
删除已经占有hash槽的结点会失败,报错如下:
[ERR] Node 127.0.0.1:7005 is not empty! Reshard data away and try again.
需要将该结点占用的hash槽分配出去。
6.5 小结(redis集群)
本部分介绍redis集群的底层原理、redis集群安装、redis集群使用、附加部分,帮助读者充分理解redis-cluster原理及使用。
七、尾声
本文主要介绍四个知识,包括redis基础知识、redis底层原理、单机版redis、集群版redis(redis-cluster),从基础知识、底层原理到开发实践,帮助读者全面学习redis。