目录
一、了解NoSql
1.1 什么是Nosql
1.2 为什么要使用NoSql
1.3 NoSql数据库的优势
1.4 常见的NoSql产品
1.5 各产品的区别
二、Redis介绍
2.1什么是Redis
2.2 Redis优势
2.3 Redis应用场景
2.4 Redis下载
三、Linux下安装Redis
3.1 环境准备
3.2 Redis的安装
3.2.1 Redis的编译环境
3.2.2 Redis的安装
3.3 Redis的启动
3.3.1 Redis的前端模式启动(了解)
3.3.2 Redis的后端启动
3.3.3 客户端访问redis
3.3.4 向Redis服务器发送命令
3.3.5 退出客户端
3.3.6 Redis的停止
3.3.7 第三方工具(redis-desktop-manager)操作redis
四、Redis数据结构
五、Redis常用指令
5.1 string类型
5.1.1 常用指令
5.1.2 应用场景之自增主键
5.2 hash类型
5.2.1 常用指令
5.2.2 string类型和hash类型的区别
5.2.3 应用之存储商品信息
5.3 list类型
5.3.1 ArrayList与LinkedList的区别
5.3.2 常用命令
5.3.3 应用之商品评论列表
5.4 set类型
5.4.1 set类型介绍
5.4.2 常用命令
5.4.3 集合运算命令
5.4.4 其他命令
5.5 zset类型 (sortedset)
5.5.1 zset介绍
5.5.2 常用命令
5.5.3 其它命令
5..5.4 应用之商品销售排行榜
5.6 HyperLogLog命令
5.6.1 HyperLogLog命令介绍
5.6.2 HyperLogLog的优点
5.6.3 HyperLogLog 相关的一些基本命令。
六、Redis的通用命令
七、Redis的事务
7.1 Redis事务介绍
7.2 Redis事务命令
7.3 事务演示
7.4 事务失败处理
八、Redis发布订阅模式
九、Jedis连接Redis
9.1 创建项目,导入依赖
9.2 链接服务器
9.2.1 方案一 :单实例链接
9.2.2 方案二:连接池
十、Redis持久化方式
10.1 什么是Redis持久化
10.2 Redis 持久化存储方式
10.2.1 RDB持久化
10.2.2 AOF持久化
10.2.3 AOF与RDB区别
十一、Redis主从复制
11.1 主从搭建步骤:
十二、Redis哨兵模式
12.1 第一步:配置哨兵:
12.2 第二步:启动哨兵:
12.3 第三步:主机宕机
十三、Redis集群方案
13.1 redis-cluster架构图
13.2 redis-cluster投票:容错
13.3 集群搭建步骤
13.4 连接集群
13.5 查看集群信息
13.6 查看集群中节点信息
13.7 Jedis连接集群
13.7.1 关闭防火墙
13.7.2 代码实现
十四、Redis高端面试-缓存穿透,缓存击穿,缓存雪崩问题
14.1 缓存的概念
14.2 缓存雪崩
14.3 缓存穿透
14.4 缓存击穿
十五、Redis高端面试-分布式锁
15.1 使用分布式锁要满足的几个条件:
15.2 什么是分布式锁?
15.3 应用的场景
15.4 使用redis的setNX命令实现分布式锁
15.4.1 实现的原理:
15.4.2 基本命令解析
一、了解NoSql
1.1 什么是Nosql
NoSQL,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在处理web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,出现了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合,多重数据种类带来的挑战,尤其是大数据应用难题。
NoSQL最常见的解释是“non-relational”, “Not Only SQL”也被很多人接受。NoSQL仅仅是一个概念,泛指非关系型的数据库,区别于关系数据库,它们不保证关系数据的ACID特性。
1.2 为什么要使用NoSql
传统的数据库遇到的瓶颈:
传统的关系数据库具有不错的性能,高稳定型,久经历史考验,而且使用简单,功能强大,同时也积累 了大量的成功案例。在互联网领域,MySQL成为了绝对靠前的王者,毫不夸张的说,MySQL为互联网 的发展做出了卓越的贡献。
在90年代,一个网站的访问量一般都不大,用单个数据库完全可以轻松应付。在那个时候,更多的都是 静态网页,动态交互类型的网站不多。
到了最近10年,网站开始快速发展。火爆的论坛、博客、sns、微博逐渐引领web领域的潮流。在初 期,论坛的流量其实也不大,如果你接触网络比较早,你可能还记得那个时候还有文本型存储的论坛程 序,可以想象一般的论坛的流量有多大。
现在网站的特点:
(1) 高并发读写
Web2.0网站,数据库并发负载非常高,往往达到每秒上万次的读写请求
(2) 高容量存储和高效存储
Web2.0网站通常需要在后台数据库中存储海量数据,如何存储海量数据并进行高效的查询往往是一个 挑战
(3) 高扩展性和高可用性
随着系统的用户量和访问量与日俱增,需要数据库能够很方便的进行扩展、维护
1.3 NoSql数据库的优势
(1) 易扩展
NoSQL数据库种类繁多,但是一个共同的特点都是去掉关系数据库的关系型特性。数据之间无关系,这 样就非常容易扩展。也无形之间,在架构的层面上带来了可扩展的能力。
(2)大数据量,高性能
NoSQL数据库都具有非常高的读写性能,尤其在大数据量下,同样表现优秀。这得益于它的无关系性, 数据库的结构简单。一般MySQL使用Query Cache,每次表的更新Cache就失效,是一种大粒度的 Cache,在针对web2.0的交互频繁的应用,Cache性能不高。而NoSQL的Cache是记录级的,是一种细 粒度的Cache,所以NoSQL在这个层面上来说就要性能高很多了。
(3)灵活的数据模型
NoSQL无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。而在关系数据库里,增删 字段是一件非常麻烦的事情。如果是非常大数据量的表,增加字段简直就是一个噩梦。这点在大数据量 的web2.0时代尤其明显。
(4) 高可用
NoSQL在不太影响性能的情况,就可以方便的实现高可用的架构。比如Cassandra,HBase模型,通过 复制模型也能实现高可用。
1.4 常见的NoSql产品
1.5 各产品的区别
二、Redis介绍
2.1什么是Redis
全称:REmote DIctionary Server(远程字典服务器)。是完全开源免费的,用C语言编写的, 遵守BCD协议。是一个高性能的(key/value)分布式内存数据库, 基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被人们称为数据结构服务器。
Redis 与其他 key - value 缓存产品有以下三个特点:
(1) Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
(2) Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
(3) Redis支持数据的备份,即master-slave(主从)模式的数据备份
2.2 Redis优势
(1) 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
(2) 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
(3) 原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
(4) 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性
(5) 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
(6) 使用多路I/O复用模型,非阻塞IO;
2.3 Redis应用场景
(1) 缓存(数据查询,短连接,新闻内容,商品内容等),使用最多
(2) 聊天室在线好友列表
(3) 任务队列(秒杀,抢购,12306等)
(4) 应用排行榜
(5) 网站访问统计
(6) 数据过期处理(可以精确到毫秒)
(7) 分布式集群架构中的session问题
2.4 Redis下载
(1)Http://redis.io/ 英文地址
(2)Http://www.redis.cn/ 中文地址
三、Linux下安装Redis
3.1 环境准备
(1)虚拟机版本:VMware® Workstation 12 Pro
(2) Linux系统:Centos Release 6.5
(3) 远程命令端:xshell
(4)文件传输工具:SecureFXPortable
3.2 Redis的安装
3.2.1 Redis的编译环境
Redis是C语言开发的,安装redis需要先去官网下载源码进行编译,编译需要依赖于GCC编译环境,如果CentOS上没有安装gcc编译环境,需要提前安装,安装命令如下:(这里我们使用root用户处理这些操作)
[root@localhost ~]# yum install gcc-c++
如果提示是否下载,选择: y
如果提示是否安装,选择: y
3.2.2 Redis的安装
(1) 使用SecureFXPortable上传Redis安装文件到Linux目录
(2)上传Redis安装文件,这里我上传自建文件夹: /home/yhp/local
(3)解压redis文件
[root@localhost local]# tar -zxvf redis-5.0.5.tar.gz
(4)编译Redis(编译,将.c文件编译为.o文件)
进入解压文件夹,执行 make
[root@localhost local]# cd redis-5.0.5
[root@localhost redis-5.0.5]# make
编译成功!如果编译过程中出错,先删除安装文件目录,后解压重新编译。
(5) 安装
[root@localhost redis-5.0.5]# make PREFIX=/home/admin/myapps/redis install
说明:这里的/home/myapps/redis 是自定义的redis安装路径
(6)安装之后的bin目录
bin文件夹下的命令:
(7) Copy文件
Redis启动需要一个配置文件,可以修改端口号信息。将redis解压的文件夹中的redis.conf文件复制到安装目录
[root@localhost redis-5.0.5]# cp redis.conf /home/admin/myapps/redis
3.3 Redis的启动
3.3.1 Redis的前端模式启动(了解)
直接运行bin/redis-server将使永前端模式启动,前端模式启动的缺点是启动完成后,不能再进行其他操作,如果要操作必须使用ctrl+c,同时redis-server程序结束,不推荐此方法。
[root@localhost bin]# ./redis-server
下面是启动界面(这个界面只能启动,启动后不能进行其他操作)
使用ctrl+c退出前端启动。
3.3.2 Redis的后端启动
修改redis.conf配置文件,设置:daemonize yes,然后可以使用后端模式启动。
[root@localhost redis]# vi redis.conf
启动时,指定配置文件(这里所在文件夹是redis)
[root@localhost redis]# ./bin/redis-server ./redis.conf
Redis默认端口:6379,通过当前服务进行查看
[root@localhost redis]# ps -ef | grep -i redis
3.3.3 客户端访问redis
如果想要通过指令来操作redis,可以使用redis的客户端进行操作,在bin文件夹下运行redis-cli
该指令默认连接的127.0.0.1 ,端口号是6379
[root@localhost bin]# ./redis-cli
127.0.0.1:6379>
如果想要连接指定的ip地址以及端口号,则需要按照
redis-cli -h ip地址 -p 端口号
3.3.4 向Redis服务器发送命令
Ping,测试客户端与Redis的连接是否正常,如果连接正常,回收到pong
127.0.0.1:6379> ping
PONG
3.3.5 退出客户端
127.0.0.1:6379> quit
3.3.6 Redis的停止
(1) 强制结束程序。强制终止Redis进程可能会导致redis持久化数据丢失。
语法:kill -9 pid (2) 正确停止Redis的方式应该是向Redis发送SHUTDOWN命令,方法为(关闭默认的端口)
3.3.7 第三方工具(redis-desktop-manager)操作redis
注意:需要关闭linux防火墙并且修改redis.conf文件中的bind参数
bind linux的ip地址
此时如果通过redis客户端访问的时候,代码如下:
./redis-cli -h 192.168.197.132 -p 6379
四、Redis数据结构
Redis 是一种基于内存的数据库,并且提供一定的持久化功能,它是一种键值(key-value)数据库,使用 key 作为 索引找到当前缓存的数据,并且返回给程序调用者。当前的 Redis 支持 6 种数据类型,它们分别是字符串(String)、列表(List)、集合(set)、哈希结构 (hash)、有序集合(zset)和基数( HyperLogLog )
五、Redis常用指令
5.1 string类型
5.1.1 常用指令
(1) 赋值
语法:SET key value
示例:
127.0.0.1:6379> set test 123 OK
(2) 取值
语法:GET key
示例:127.0.0.1:6379> get test "123"
(3) 取值并赋值
语法:
GETSET key value
示例:
127.0.0.1:6379> getset s2 222 "111" 127.0.0.1:6379> get s2 "222"
(4) 数值增减
注意事项 :
1 、 当 value 为整数数据时,才能使用以下命令操作数值的增减。2 、 数值递增都是【原子】操作。3 、 redis 中的每一个单独的命令都是原子性操作。当多个命令一起执行的时候,就不能保证 原子性,不过我们可以使 用事务和lua 脚本来保证这一点。
非原子性操作示例:
int i = 1; i++; System.out.println(i)
(5) 递增数字
语法(increment):
INCR key
示例:127.0.0.1:6379> incr num (integer) 1 127.0.0.1:6379> incr num (integer) 2 127.0.0.1:6379> incr num (integer) 3
(6) 增加指定的整数
语法:INCRBY key increment
示例:127.0.0.1:6379> incrby num 2 (integer) 5 127.0.0.1:6379> incrby num 2 (integer) 7 127.0.0.1:6379> incrby num 2 (integer) 9
(7) 递减数值
语法:DECR key
示例:127.0.0.1:6379> incr num (integer) 1 127.0.0.1:6379> incr num (integer) 2 127.0.0.1:6379> incr num (integer) 3
(8) 减少指定的整数
语法:DECRBY key decrement
示例:127.0.0.1:6379> decr num (integer) 6 127.0.0.1:6379> decr num (integer) 5 127.0.0.1:6379> decrby num 3 (integer) 2 127.0.0.1:6379> decrby num 3 (integer) -1
(9) 仅当不存在时赋值
使用该命令可以实现【分布式锁】的功能,后续讲解!!!语法:setnx key value
示例:redis> EXISTS job # job 不存在 (integer) 0 redis> SETNX job "programmer" # job 设置成功 (integer) 1 redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败 (integer) 0 redis> GET job # 没有被覆盖 "programmer"
(10) 其它命令
1.向尾部追加值
APPEND 命令,向键值的末尾追加 value 。如果键不存在则将该键的值设置为 value ,即相当于 SET key value 。返回值是追加后字符串的总长度。语法:APPEND key value
示例:127.0.0.1:6379> set str hello OK 127.0.0.1:6379> append str " world!" (integer) 12 127.0.0.1:6379> get str "hello world!"
2.获取字符串长度
STRLEN 命令,返回键值的长度,如果键不存在则返回0。语法:STRLEN key
示例:127.0.0.1:6379> strlen str (integer) 0 127.0.0.1:6379> set str hello OK 127.0.0.1:6379> strlen str (integer) 5
3.同时设置/获取多个键值
语法:MSET key value [key value …] MGET key [key …]
示例:127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 OK 127.0.0.1:6379> get k1 "v1" 127.0.0.1:6379> mget k1 k3 1) "v1" 2) "v3"
5.1.2 应用场景之自增主键
需求:商品编号、订单号采用 INCR 命令生成。设计: key 命名要有一定的设计实现:定义商品编号 key : items:id
192.168.101.3:7003> INCR items:id (integer)
2
192.168.101.3:7003> INCR items:id (integer)
3
5.2 hash类型
hash 类型也叫 散列类型,它提供了字段和字段值的映射。字段值只能是字符串类型,不支持散列类型、集合类型等 其它类型。如下:
5.2.1 常用指令
(1) 赋值
HSET 命令不区分插入和更新操作,当执行插入操作时 HSET 命令返回 1 ,当执行更新操作时返回 0 。
语法:HSET key field value
示例:127.0.0.1:6379> hset user username zhangsan (integer) 1
语法:HMSET key field value [field value ...]
示例:127.0.0.1:6379> hmset user age 20 username lisi OK
类似 HSET ,区别在于如果字段存在,该命令不执行任何操作语法:HSETNX key field value
示例:127.0.0.1:6379> hsetnx user age 30 # 如果user中没有age字段则设置age值为30,否则不做任何操作 (integer) 0
(2) 取值
1.获取一个字段值
语法:HGET key field
示例:127.0.0.1:6379> hget user username "zhangsan“
2.获取多个字段值
语法:HMGET key field [field ...] 1
示例:127.0.0.1:6379> hmget user age username 1) "20" 2) "lisi"
3.获取所有字段值
语法:HGETALL key
示例:127.0.0.1:6379> hgetall user 1) "age" 2) "20" 3) "username" 4) "lisi"
(3) 删除字段
可以删除一个或多个字段,返回值是被删除的字段个数语法:HDEL key field [field ...]
示例:127.0.0.1:6379> hdel user age (integer) 1 127.0.0.1:6379> hdel user age name (integer) 0 127.0.0.1:6379> hdel user age username (integer) 1
(4) 增加数字
语法:HINCRBY key field increment
示例:127.0.0.1:6379> hincrby user age 2 # 将用户的年龄加2 (integer) 22 127.0.0.1:6379> hget user age # 获取用户的年龄 "22“
(5) 其它命令
1.判断字段是否存在
语法:HEXISTS key field
示例:127.0.0.1:6379> hexists user age 查看user中是否有age字段 (integer) 1 127.0.0.1:6379> hexists user name 查看user中是否有name字段 (integer) 0
2.只获取字段名或字段值
语法:HKEYS key HVALS key
示例:127.0.0.1:6379> hmset user age 20 name lisi OK 127.0.0.1:6379> hkeys user 1) "age" 2) "name" 127.0.0.1:6379> hvals user 1) "20" 2) "lisi"
3.获取字段数量
语法:HLEN key
示例:127.0.0.1:6379> hlen user (integer) 2
4.获取所有字段
获得 hash 的所有信息,包括 key 和 value语法:hgetall key
5.2.2 string类型和hash类型的区别
hash类型适合存储那些对象数据,特别是对象属性经常发生【增删改】操作的数据。 string类型也可以存储对象数 据,将java对象转成json字符串进行存储,这种存储适合【查询】操作。
5.2.3 应用之存储商品信息
商品信息字段
【商品id、商品名称、商品描述、商品库存、商品好评】
定义商品信息的key商品ID为1001的信息在 Redis中的key为:[items:1001]
存储商品信息192.168.101.3:7003> HMSET items:1001 id 3 name apple price 999.9 OK
获取商品信息192.168.101.3:7003> HGET items:1001 id "3" 192.168.101.3:7003> HGETALL items:1001 1) "id" 2) "3" 3) "name" 4) "apple" 5) "price" 6) "999.9"
5.3 list类型
Redis 的列表类型( list 类型)可以 存储一个有序的字符串列表 ,常用的操作是向列表两端添加元素,或者获得列表 的某一个片段。列表类型内部是使用 双向链表( double linked list ) 实现的,所以向列表两端添加元素的时间复杂度为 o(1) ,获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记 录也是极快的。
5.3.1 ArrayList与LinkedList的区别
ArrayList 使用数组方式存储数据,所以根据索引查询数据速度快,而新增或者删除元素时需要设计到位移操作, 所以比较慢。LinkedList 使用双向链表方式存储数据,每个元素都记录前后元素的指针,所以插入、删除数据时只是更改前后元 素的指针指向即可,速度非常快。然后通过下标查询元素时需要从头开始索引,所以比较慢,但是如果查询前几个元 素或后几个元素速度比较快。
5.3.2 常用命令
(1) LPUSH/RPUSH
语法:LPUSH key value [value ...] RPUSH key value [value ...]
示例:127.0.0.1:6379> lpush list:1 1 2 3 (integer) 3 127.0.0.1:6379> rpush list:1 4 5 6 (integer) 3
(2) LRANGE
获取列表中的某一片段。将返回 `start` 、 `stop` 之间的所有元素(包含两端的元素),索引从`0` 开始。索引可以 是负数,如:“`-1`” 代表最后边的一个元素。
语法:LRANGE key start stop
示例:127.0.0.1:6379> lrange list:1 0 2 1) "2" 2) "1" 3) "4"
(3) LPOP/RPOP
从列表两端弹出元素从列表左边弹出一个元素,会分两步完成:第一步是将列表左边的元素从列表中移除第二步是返回被移除的元素值。
语法:LPOP key RPOP key
示例 :127.0.0.1:6379>lpop list:1 "3" 127.0.0.1:6379>rpop list:1 "6"
(4) LLEN
获取列表中元素的个数语法:llen key
示例:127.0.0.1:6379> llen list:1 (integer) 2
(5) 其它命令
1. LREM
删除列表中指定个数的值
- 当 count>0 时, LREM 会从列表左边开始删除。- 当 count<0 时, LREM 会从列表后边开始删除。- 当 count=0 时, LREM 删除所有值为 value 的元素。
语法:LREM key count value
2. LINDEX
获得指定索引的元素值语法:LINDEX key index
示例:127.0.0.1:6379>lindex l:list 2 "1"
3. 设置指定索引的元素值
语法:LSET key index value
示例:
127.0.0.1:6379> lset l:list 2 2 OK 127.0.0.1:6379> lrange l:list 0 -1 1) "6" 2) "5" 3) "2" 4) "2"
4. LTRIM
只保留列表指定片段 , 指定范围和 LRANGE 一致语法:LTRIM key start stop
示例:127.0.0.1:6379> lrange l:list 0 -1 1) "6" 2) "5" 3) "0" 4) "2" 127.0.0.1:6379> ltrim l:list 0 2 OK 127.0.0.1:6379> lrange l:list 0 -1 1) "6" 2) "5" 3) "0"
5. LINSERT
向列表中插入元素。该命令首先会在列表中从左到右查找值为 pivot 的元素,然后根据第二个参数是BEFORE 还是 AFTER 来决定将 value 插 入到该元素的前面还是后面。语法:LINSERT key BEFORE|AFTER pivot value
示例:127.0.0.1:6379> lrange list 0 -1 1) "3" 2) "2" 3) "1" 127.0.0.1:6379> linsert list after 3 4 (integer) 4 127.0.0.1:6379> lrange list 0 -1 1) "3" 2) "4" 3) "2" 4) "1"
6. RPOPLPUSH
将元素从一个列表转移到另一个列表中语法:RPOPLPUSH source destination
示例:
127.0.0.1:6379> rpoplpush list newlist "1" 127.0.0.1:6379> lrange newlist 0 -1 1) "1" 127.0.0.1:6379> lrange list 0 -1 1) "3" 2) "4" 3) "2"
5.3.3 应用之商品评论列表
用户针对某一商品发布评论,一个商品会被不同的用户进行评论,存储商品评论时,要按时间顺序排序。用户在前端页面查询该商品的评论,需要按照时间顺序降序排序。
使用 list 存储商品评论信息, KEY 是该商品的 ID , VALUE 是商品评论信息列表
192.168.101.3:7001> LPUSH items:comment:1001 '{"id":1,"name":"商品不错,很 好!!","date":1430295077289}'
5.4 set类型
5.4.1 set类型介绍
set 类型即集合类型,其中的数据是不重复且没有顺序。
集合类型和列表类型的对比:集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型的 Redis 内部是使用值 为空的散列表实现,所有这些操作的时间复杂度都为 0(1) 。Redis 还提供了多个集合之间的 交集、并集、差集 的运算。
5.4.2 常用命令
(1) SADD/SREM 添加元素/删除元素
语法:SADD key member [member ...] SREM key member [member ...]
示例:127.0.0.1:6379> sadd set a b c (integer) 3 127.0.0.1:6379> sadd set a (integer) 0 127.0.0.1:6379> srem set c d (integer) 1
(2) SMEMBERS 获得集合中的所有元素
语法:SMEMBERS key
示例:127.0.0.1:6379> smembers set 1) "b" 2) "a”
(3) SISMEMBER 判断元素是否在集合中
语法:SISMEMBER key member
示例:127.0.0.1:6379>sismember set a (integer) 1 127.0.0.1:6379>sismember set h (integer) 0
5.4.3 集合运算命令
(1) SDIFF 集合的差集运算 A-B:属于A并且不属于B的元素构成的集合。
语法:SDIFF key [key ...]
示例:127.0.0.1:6379> sadd setA 1 2 3 (integer) 3 127.0.0.1:6379> sadd setB 2 3 4 (integer) 3 127.0.0.1:6379> sdiff setA setB 1) "1" 127.0.0.1:6379> sdiff setB setA 1) "4"
(2) SINTER 集合的交集运算 A ∩ B:属于A且属于B的元素构成的集合。
语法:
SINTER key [key ...]
示例:127.0.0.1:6379> sinter setA setB 1) "2" 2) "3"
(3) SUNION 集合的并集运算 A ∪ B:属于A或者属于B的元素构成的集合
语法:SUNION key [key ...]
示例:127.0.0.1:6379> sunion setA setB 1) "1" 2) "2" 3) "3" 4) "4"
5.4.4 其他命令
(1) SCARD 获得集合中元素的个数
语法:SCARD key
示例:127.0.0.1:6379> smembers setA 1) "1" 2) "2" 3) "3" 127.0.0.1:6379> scard setA (integer) 3
(2) SPOP
语法:SPOP key
示例:127.0.0.1:6379> spop setA "1"
5.5 zset类型 (sortedset)
5.5.1 zset介绍
在 set 集合类型的基础上,有序集合类型为集合中的每个元素都 关联一个分数 ,这使得我们不仅可以完成插入、删除 和判断元素是否存在在集合中,还能够获得分数最高或最低的前N个元素、获取指定分数范围内的元素等与分数有关 的操作。
1 、二者都是有序的。2 、二者都可以获得某一范围的元素。
但是,二者有着很大区别:
1 、列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会变慢。2 、有序集合类型使用散列表实现,所有即使读取位于中间部分的数据也很快。3 、列表中不能简单的调整某个元素的位置,但是有序集合可以(通过更改分数实现)4 、有序集合要比列表类型更耗内存。
5.5.2 常用命令
(1) ZADD
增加元素。向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。返回值是新加入到 集合中的元素个数,不包含之前已经存在的元素。
语法:
ZADD key score member [score member ...]
示例:
127.0.0.1:6379> zadd scoreboard 80 zhangsan 89 lisi 94 wangwu (integer) 3 127.0.0.1:6379> zadd scoreboard 97 lisi (integer) 0
(2) ZRANGE/ZREVRANGE
获得排名在某个范围的元素列表。
- ZRANGE:按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
- ZREVRANGE:按照元素分数从大到小的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
语法:ZRANGE key start stop [WITHSCORES] ZREVRANGE key start stop [WITHSCORES]
示例:127.0.0.1:6379> zrange scoreboard 0 2 1) "zhangsan" 2) "wangwu" 3) "lisi“ 127.0.0.1:6379> zrevrange scoreboard 0 2 1) " lisi " 2) "wangwu" 3) " zhangsan “
如果需要获得元素的分数的可以在命令尾部加上 WITHSCORES 参数127.0.0.1:6379> zrange scoreboard 0 1 WITHSCORES 1) "zhangsan" 2) "80" 3) "wangwu" 4) "94"
语法:ZSCORE key member
示例:127.0.0.1:6379> zscore scoreboard lisi "97"
(4) ZREM
删除元素。移除有序集合 key 中的一个或多个成员,不存在的成员将被忽略。当 key 存在但不是有序集类型时,返回一个错误。
语法:ZREM key member [member ...]
示例:127.0.0.1:6379> zrem scoreboard lisi (integer) 1
5.5.3 其它命令
语法:ZRANGEBYSCORE key min max [WITHSCORES]
示例:127.0.0.1:6379> ZRANGEBYSCORE scoreboard 90 97 WITHSCORES 1) "wangwu" 2) "94" 3) "lisi" 4) "97" 127.0.0.1:6379> ZRANGEBYSCORE scoreboard 70 100 limit 1 2 1) "wangwu" 2) "lisi"
(2) ZINCRBY
增加某个元素的分数。返回值是更改后的分数
语法:ZINCRBY key increment member
示例:127.0.0.1:6379> ZINCRBY scoreboard 4 lisi "101"
(3) ZCARD 获得集合中元素的数量。
语法:ZCARD key
示例:127.0.0.1:6379> ZCARD scoreboard (integer) 3
(4) ZCOUNT 获得指定分数范围内的元素个数
语法:ZCOUNT key min max
示例:127.0.0.1:6379> ZCOUNT scoreboard 80 90 (integer) 1
(5) ZREMRANGEBYRANK 按照排名范围删除元素
语法:ZREMRANGEBYRANK key start stop
示例:127.0.0.1:6379> ZREMRANGEBYRANK scoreboard 0 1 (integer) 2 127.0.0.1:6379> ZRANGE scoreboard 0 -1 1) "lisi"
(6) ZREMRANGEBYSCORE 按照分数范围删除元素
语法:ZREMRANGEBYSCORE key min max
示例:127.0.0.1:6379> zadd scoreboard 84 zhangsan (integer) 1 127.0.0.1:6379> ZREMRANGEBYSCORE scoreboard 80 100 (integer) 1
(7) ZRANK/ZREVRANK
获取元素的排名。- ZRANK :从小到大- ZREVRANK :从大到小
语法:ZRANK key member ZREVRANK key member
示例:127.0.0.1:6379> ZRANK scoreboard lisi (integer) 0 127.0.0.1:6379> ZREVRANK scoreboard zhangsan (integer) 1
5..5.4 应用之商品销售排行榜
根据商品销售量对商品进行排行显示
定义商品销售排行榜( sorted set 集合), Key 为 items:sellsort ,分数为商品销售量。
192.168.101.3:7007> ZADD items:sellsort 9 1001 10 1002
192.168.101.3:7001> ZINCRBY items:sellsort 1 1001
192.168.101.3:7001> ZREVRANGE items:sellsort 0 9 withscores
5.6 HyperLogLog命令
5.6.1 HyperLogLog命令介绍
HyperLogLog 是一种使用随机化的算法,以少量内存提供集合中唯一元素数量的近似值。HyperLogLog 可以接受多个元素作为输入,并给出输入元素的基数估算值:基数:集合中不同元素的数量。比如 {‘apple’, ‘banana’, ‘cherry’, ‘banana’, ‘apple’} 的基数就是3。 估算值:算法给出的基数并不是精确的,可能会比实际稍微多一些或者稍微少一些,但会控制在合理的范围之内。
5.6.2 HyperLogLog的优点
HyperLogLog 的优点是,即使输入元素的数量或者体积非常非常大,计算基数所需的空间总是固定的、并且是很 小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计 算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像 集合那样,返回输入的各个元素。
5.6.3 HyperLogLog 相关的一些基本命令。
命令
|
说明
|
PFADD key element [element …]
|
将指定的元素添加到指定的
HyperLogLog
中
|
PFCOUNT key [key …]
|
返回给定
HyperLogLog
的基数估算值
|
PFMERGE destkey sourcekey [sourcekey …]
|
将多个
HyperLogLog
合并为一个
HyperLogLog
|
示例:
redis 127.0.0.1:6379> PFADD mykey "redis" 1) (integer) 1 redis 127.0.0.1:6379> PFADD mykey "java" 1) (integer) 1 redis 127.0.0.1:6379> PFADD mykey "mysql" 1) (integer) 1 redis 127.0.0.1:6379> PFCOUNT mykey (integer) 3
六、Redis的通用命令
(1) keys
返回满足给定 pattern 的所有 key语法:keys pattern
示例:redis 127.0.0.1:6379> keys mylist* 1) "mylist" 2) "mylist5" 3) "mylist6" 4) "mylist7" 5) "mylist8"
(2) del
语法:DEL key
示例:127.0.0.1:6379> del test (integer) 1
(3) exists 确认一个key 是否存在
语法:exists key
示例:从结果来看,数据库中不存在 HongWan 这个 key ,但是 age 这个 key 是存在的redis 127.0.0.1:6379> exists HongWan (integer) 0 redis 127.0.0.1:6379> exists age (integer) 1
(4) expire(重点)
Redis 在实际使用过程中更多的用作缓存,然而缓存的数据一般都是需要设置生存时间的,即:到期后数据销毁。
语法:EXPIRE key seconds 设置key的生存时间(单位:秒)key在多少秒后会自动删除 TTL key 查看key生于的生存时间 PERSIST key 清除生存时间 PEXPIRE key milliseconds 生存时间设置单位为:毫秒
示例:192.168.101.3:7002> set test 1 设置test的值为1 OK 192.168.101.3:7002> get test 获取test的值 "1" 192.168.101.3:7002> EXPIRE test 5 设置test的生存时间为5秒 (integer) 1 192.168.101.3:7002> TTL test 查看test的生于生成时间还有1秒删除 (integer) 1 192.168.101.3:7002> TTL test (integer) -2 192.168.101.3:7002> get test 获取test的值,已经删除 (nil)
(5) rename 重命名key
语法:rename oldkey newkey
示例: age 成功的被我们改名为 age_new 了redis 127.0.0.1:6379[1]> keys * 1) "age" redis 127.0.0.1:6379[1]> rename age age_new OK redis 127.0.0.1:6379[1]> keys * 1) "age_new"
(6) type 显示指定key的数据类型
语法:type key
示例:这个方法可以非常简单的判断出值的类型redis 127.0.0.1:6379> type addr string redis 127.0.0.1:6379> type myzset2 zset redis 127.0.0.1:6379> type mylist list
七、Redis的事务
7.1 Redis事务介绍
Redis 的事务是通过 MULTI 、 EXEC 、 DISCARD 和 WATCH 、UNWATCH这五个命令来完成的。
Redis 的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
Redis 将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
Redis 不支持回滚操作。
7.2 Redis事务命令
(1) MULTI
用于标记事务块的开始。Redis 会将后续的命令逐个放入队列中,然后使用 EXEC 命令原子化地执行这个命令序列。
语法: multi
(2) EXEC
在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态
语法: exec
(3) DISCARD
清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
语法: discard
(4) WATCH
当某个 [ 事务需要按条件执行 ] 时,就要使用这个命令将给定的 [ 键设置为受监控 ] 的状态。
语法:watch key [key…]
(5) UNWATCH
清除所有先前为一个事务监控的键。
语法: unwatch
7.3 事务演示
示例一:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 111
QUEUED
127.0.0.1:6379> hset set1 name zhangsan
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 1
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s2 222
QUEUED
127.0.0.1:6379> hset set2 age 20
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> watch s1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 555
QUEUED
127.0.0.1:6379> exec # 此时在没有exec之前,通过另一个命令窗口对监控的s1字段进行修改
(nil)
127.0.0.1:6379> get s1
111
示例二:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set u1 user1
QUEUED
127.0.0.1:6379> get u1
QUEUED
127.0.0.1:6379> sadd tag c++ html java
QUEUED
127.0.0.1:6379> smembers tag
QUEUED
127.0.0.1:6379> exec
1) OK
2) "user1"
3) (integer) 3
4) 1) "java"
2) "html"
3) "c++"
7.4 事务失败处理
(1) Redis 语法错误(编译期)
(2) Redis 运行错误
(3) Redis 不支持事务回滚(为什么呢)
1、大多数事务失败是因为 语法错误或者类型错误 ,这两种错误,在开发阶段都是可以预见的2、 Redis 为了 性能方面 就忽略了事务回滚。
八、Redis发布订阅模式
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。
127.0.0.1:6379> subscribe redisMessage
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisMessage"
3) (integer) 1
127.0.0.1:6379> publish redisMessage "demo1 test"
(integer) 1
127.0.0.1:6379> publish redisMessage "demo2 test"
(integer) 1
127.0.0.1:6379> publish redisMessage "demo3 test"
(integer) 1
127.0.0.1:6379> subscribe redisMessage
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisMessage"
3) (integer) 1
1) "message"
2) "redisMessage"
3) "demo1 test"
1) "message"
2) "redisMessage"
3) "demo2 test"
1) "message"
2) "redisMessage"
3) "demo3 test"
九、Jedis连接Redis
9.1 创建项目,导入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
</dependency>
service iptables stop
service iptables status
9.2 链接服务器
9.2.1 方案一 :单实例链接
Jedis jedis = new Jedis(“ip地址”, 端口号);//建立链接
public static void main(String[] args) {
Jedis jedis=new Jedis("192.168.197.129",6379);
//设置值
jedis.set("java001","java工程师");
String java001 = jedis.get("java001");
System.out.println(java001);
}
解决方案:
虚拟机客户端连接的 ip 是 127.0.0.1, 意思是连接的本机 , 其他机器无法连接 , 这里需要修改配置文件 , 将连接地址改为虚拟机的地址, 就可以了修改 redis.conf 文件里面的 bind 连接地址 , 将连接地址改为自己虚拟机的 ipbind 192.168.197.129
服务器上存储:
9.2.2 方案二:连接池
// 1.获取连接池配置对象,设置配置项
JedisPoolConfig config = new JedisPoolConfig();
// 1.1最大的连接数
config.setMaxTotal(30);
// 1.2最大的空闲
config.setMaxIdle(10);
// 2.获取连接池
JedisPool jedisPool = new JedisPool(config, "192.168.197.129", 6379);
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
// 3.设置数据
jedis.set("name", "张三");
String name = jedis.get("name");
System.out.println("name=" + name);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
// 4.虚拟机关闭的时候,释放资源
if (jedisPool != null) {
jedisPool.close();
}
}
十、Redis持久化方式
10.1 什么是Redis持久化
由于 redis 的值放在内存中,为防止突然断电等特殊情况的发生,需要对数据进行持久化备份。即将内存数据保存到硬盘。
10.2 Redis 持久化存储方式
10.2.1 RDB持久化
优点:使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能缺点: RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合 数据要求不严谨的时候
#dbfilename:持久化数据存储在本地的文件
dbfilename dump.rdb
#dir:持久化数据存储在本地的路径,如果是在/redis/redis-5.0.5/src下启动的redis-cli,则数据会存储在当前src目录下
dir ./
##snapshot触发的时机,save
##如下为900秒后,至少有一个变更操作,才会snapshot
##对于此值的设置,需要谨慎,评估系统的变更操作密集程度
##可以通过“save”来关闭snapshot功能
#save时间,以下分别表示更改了1个key时间隔900s进行持久化存储;更改了10个key300s进行存储;更改10000个key60s进行存储。
save 900 1
save 300 10
save 60 10000
##当snapshot时出现错误无法继续时,是否阻塞客户端“变更操作”,“错误”可能因为磁盘已满/磁盘故障/OS级别异常等
stop-writes-on-bgsave-error yes
##是否启用rdb文件压缩,默认为“yes”,压缩往往意味着“额外的cpu消耗”,同时也意味这较小的文件尺寸以及较短的网络传输时间
rdbcompression yes
10.2.2 AOF持久化
Append-Only File ,将 “ 操作 + 数据 ” 以格式化指令的方式追加到操作日志文件的尾部,在 append 操作返回后 ( 已经 写入到文件或者将要写入) ,才进行实际的数据变更, “ 日志文件 ” 保存了历史所有的操作过程;当 server 需要数据 恢复时,可以直接 replay 此日志文件,即可还原所有的操作过程。 AOF 相对可靠, AOF 文件内容是字符串,非常 容易阅读和解析。
优点:可以保持更高的数据完整性,如果设置追加 fifile 的时间是 1s ,如果 redis 发生故障,最多会丢失 1s 的数 据;且如果日志写入不完整支持 redis-check-aof 来进行日志修复; AOF 文件没被 rewrite 之前(文件过大时会对 命令进行合并重写),可以删除其中的某些命(比如误操作的 flflushall )。缺点: AOF 文件比 RDB 文件大,且恢复速度慢。
##此选项为aof功能的开关,默认为“no”,可以通过“yes”来开启aof功能
##只有在“yes”下,aof重写/文件同步等特性才会生效
appendonly yes
##指定aof文件名称
appendfilename appendonly.aof
##指定aof操作中文件同步策略,有三个合法值:always everysec no,默认为everysec
appendfsync everysec
##在aof-rewrite期间,appendfsync是否暂缓文件同步,"no"表示“不暂缓”,“yes”表示“暂缓”,默认为“no”
no-appendfsync-on-rewrite no
##aof文件rewrite触发的最小文件尺寸(mb,gb),只有大于此aof文件大于此尺寸是才会触发rewrite,默认“64mb”,建议“512mb”
auto-aof-rewrite-min-size 64mb
##相对于“上一次”rewrite,本次rewrite触发时aof文件应该增长的百分比。
##每一次rewrite之后,redis都会记录下此时“新aof”文件的大小(例如A),那么当aof文件增长到A*(1 + p)之后
##触发下一次rewrite,每一次aof记录的添加,都会检测当前aof文件的尺寸。
auto-aof-rewrite-percentage 100
always :每一条 aof 记录都立即同步到文件,这是最安全的方式,也以为更多的磁盘操作和阻塞延迟,是 IO 开支 较大。everysec :每秒同步一次,性能和安全都比较中庸的方式,也是 redis 推荐的方式。如果遇到物理服务器故障,有 可能导致最近一秒内 aof 记录丢失 ( 可能为部分丢失 ) 。no : redis 并不直接调用文件同步,而是交给操作系统来处理,操作系统可以根据 buffffer 填充情况 / 通道空闲时间 等择机触发同步;这是一种普通的文件操作方式。性能较好,在物理服务器故障时,数据丢失量会因 OS 配置有 关。其实,我们可以选择的太少, everysec 是最佳的选择。如果你非常在意每个数据都极其可靠,建议你选择一款 “ 关 系性数据库” 。
10.2.3 AOF与RDB区别
RDB 是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复优点:使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能缺点: RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合 数据要求不严谨的时候
Append-only fifile ,将 “ 操作 + 数据 ” 以格式化指令的方式追加到操作日志文件的尾部,在 append 操作返回后 ( 已经写 入到文件或者即将写入) ,才进行实际的数据变更, “ 日志文件 ” 保存了历史所有的操作过程;当 server 需要数据恢复 时,可以直接replay 此日志文件,即可还原所有的操作过程。 AOF 相对可靠,它和 mysql 中 bin.log 、 apache.log 、 zookeeper中 txn-log 简直异曲同工。 AOF 文件内容是字符串,非常容易阅读和解析。优点:可以保持更高的数据完整性,如果设置追加 fifile 的时间是 1s ,如果 redis 发生故障,最多会丢失 1s 的数据;且 如果日志写入不完整支持redis-check-aof 来进行日志修复; AOF 文件没被 rewrite 之前(文件过大时会对命令进行 合并重写),可以删除其中的某些命令(比如误操作的flflushall )。缺点: AOF 文件比 RDB 文件大,且恢复速度慢。
十一、Redis主从复制
持久化保证了即使 redis 服务重启也不会丢失数据,但是当 redis 服务器的硬盘损坏了可能会导致数据丢失,通过redis 的主从复制机制就可以避免这种单点故障(单台服务器的故障)。主 redis 中的数据和从上的数据保持实时同步 , 当主 redis 写入数据时通过主从复制机制复制到两个从服务上。主从复制不会阻塞 master ,在同步数据时, master 可以继续处理 client 请求 .主机 master 配置 : 无需配置
11.1 主从搭建步骤:
[root@localhost myapps]# cp redis/ redis1 -r
[root@localhost myapps]# ll
总用量 40
drwxr-xr-x. 3 root root 4096 2月 1 09:26 redis
drwxr-xr-x. 3 root root 4096 2月 1 09:27 redis1
(3) 第三步:修改从机的port地址为6380
(4) 第四步:清除从机中的持久化文件
[root@localhost bin]# rm -rf appendonly.aof dump.rdb
[root@localhost bin]# ll
总用量 15440
-rwxr-xr-x. 1 root root 4588902 7月 1 09:27 redis-benchmark
-rwxr-xr-x. 1 root root 22225 7月 1 09:27 redis-check-aof
-rwxr-xr-x. 1 root root 45443 7月 1 09:27 redis-check-dump
-rwxr-xr-x. 1 root root 4691809 7月 1 09:27 redis-cli
lrwxrwxrwx. 1 root root 12 7月 1 09:27 redis-sentinel -> redis-server
-rwxr-xr-x. 1 root root 6450337 7月 1 09:27 redis-server
(5) 第五步:启动从机
[root@localhost redis1]# ./bin/redis-server ./redis.conf
(6) 第六步:启动6380的客户端
[root@localhost redis1]# ./bin/redis-cli -p 6380
127.0.0.1:6380> keys *
1) "mylist"
2) "num"
3) "bookCate1"
4) "newbook"
127.0.0.1:6380>
1.主机一旦发生增删改操作,那么从机会自动将数据同步到从机中2. 从机不能执行写操作 , 只能读
127.0.0.1:6380> get username
"hehe"
127.0.0.1:6380> set username haha
(error) READONLY You can't write against a read only slave.
(7) 复制的过程原理
1.当从库和主库建立 MS(master slaver) 关系后,会向主数据库发送 SYNC 命令;2.主库接收到 SYNC 命令后会开始在后台保存快照( RDB 持久化过程),并将期间接收到的写命令缓存起来;3.快照完成后 , 主 Redis 会将快照文件和所有缓存的写命令发送给从 Redis ;4.从 Redis 接收到后,会载入快照文件并且执行收到的缓存命令;5.主 Redis 每当接收到写命令时就会将命令发送从 Redis ,保证数据的一致;【内部完成 , 所以 不支持客户端在从 机人为写数据 。】
从Redis宕机 : 重启就好主 Redis 宕机 : 从数据库 ( 从机 ) 中执行 SLAVEOF NO ONE 命令,断开主从关系并且提升为主库继续服务 [ 把一个从做为 主机,这个时候新主机[ 之前的从机 ] 就具备写入的能力 ] ;主服务器修好后,重新启动后,执行 SLAVEOF 命令,将其 设置为从库[ 老主机设置为从机 ] 。 [ 手动执行,过程复杂,容易出错。 ] 是否有更好的方案?
十二、Redis哨兵模式
1. 监控主数据库和从数据库是否运行正常;2. 主数据出现故障后自动将从数据库转化为主数据库;
12.1 第一步:配置哨兵:
1. 启动哨兵进程,首先需要创建哨兵配置文件 vi sentinel.conf, 可从源码配置 redis5.0.5/sentinel.conf 中复制内容,也可以直接自定义该文件到bin 目录下2. 在配置中输入 :sentinel monitor mastername 内网 IP(127.0.0.1) 6379 13. 说明:4. mastername 监控主数据的名称,自定义5. 127.0.0.1 :监控主数据库的 IP;6. 6379: 端口7. 1 :最低通过票数
12.2 第二步:启动哨兵:
把日志写入指定的文件
[root@localhost bin]# ./redis-sentinel ./sentinel.conf >sent.log &
[1] 3373
[root@localhost bin]# ./redis-server sentinel.conf --sentinel
同时多了哨兵进程:
查询配置文件sentinel.conf中生成的内容:
12.3 第三步:主机宕机
杀死主机:kill -9 pid
[root@localhost redis6380]# kill -9 3342
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:192.168.197.129
master_port:6379
replicaof 127.0.0.1 6380
总结 :主从集群:主机有写入权限。从机没有,只有可读。意外宕机方案 :手动恢复:人为重启服务器,主机宕,把从机设置为主机。自动恢复:使用哨兵监控。自动切换主从。
十三、Redis集群方案
13.1 redis-cluster架构图
架构细节:
(1) 所有的 redis 节点彼此互联 (PING-PONG 机制 ), 内部使用二进制协议优化传输速度和带宽 .(2) 节点的 fail 是通过集群中超过半数的节点检测有效时整个集群才生效 .(3) 客户端与 redis 节点直连 , 不需要中间 proxy 层 . 客户端不需要连接集群所有节点 , 连接集群中 任何一个可用节点即可(4)redis-cluster 把所有的物理节点映射到 [ 0-16383 ]slot 上 ,cluster 负责维护 node<->slot<- >valueRedis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时, redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽, redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
13.2 redis-cluster投票:容错
心跳机制
(1) 集群中所有 master 参与投票 , 如果半数以上 master 节点与其中一个 master 节点通信超过 (cluster-node-timeout), 认为该master 节点挂掉 .(2): 什么时候整个集群不可用 (cluster_state:fail)?a: 如果集群任意 master 挂掉 , 且当前 master 没有 slave ,则集群进入 fail 状态。也可以理解成集群的 [0-16383]slot 映 射不完全时进入 fail 状态。b:如果集群超过半数以上 master 挂掉,无论是否有 slave ,集群进入 fail 状态。
13.3 集群搭建步骤
(1) 第一步:安装redis
[root@localhost redis]# mkdir redis-cluster
(3) 第三步:在集群目录下创建节点目录
[root@localhost myapps]# cp redis/ redis-cluster/7001 -r
[root@localhost myapps]# cd redis-cluster/7001
[root@localhost 7001]# ll
drwxr-xr-x. 2 root root 4096 7月 1 10:22 bin
-rw-r--r--. 1 root root 3446 7月 1 10:22 dump.rdb
-rw-r--r--. 1 root root 41404 7月 1 10:22 redis.conf
(4) 第四步:如果存在持久化文件,则删除
[root@localhost 7001]# rm -rf appendonly.aof dump.rdb
(5) 第五步:修改redis.conf配置文件,打开Cluster-enable yes
(6) 第六步:修改端口
(7) 第七步:复制出7002-7006机器
[root@localhost redis-cluster]# cp 7001/ 7002 -r
[root@localhost redis-cluster]# cp 7001/ 7003 -r
[root@localhost redis-cluster]# cp 7001/ 7004 -r
[root@localhost redis-cluster]# cp 7001/ 7005 -r
[root@localhost redis-cluster]# cp 7001/ 7006 -r
[root@localhost redis-cluster]# ll
total 28
drwxr-xr-x. 3 root root 4096 Jun 2 00:02 7001
drwxr-xr-x. 3 root root 4096 Jun 2 00:02 7002
drwxr-xr-x. 3 root root 4096 Jun 2 00:02 7003
drwxr-xr-x. 3 root root 4096 Jun 2 00:03 7004
drwxr-xr-x. 3 root root 4096 Jun 2 00:03 7005
drwxr-xr-x. 3 root root 4096 Jun 2 00:03 7006
-rwxr-xr-x. 1 root root 3600 Jun 1 23:52 redis-trib.rb
(8) 第八步:修改7002-7006机器的端口
cd 7001
./bin/redis-server ./redis.conf
cd ..
cd 7002
./bin/redis-server ./redis.conf
cd ..
cd 7003
./bin/redis-server ./redis.conf
cd ..
cd 7004
./bin/redis-server ./redis.conf
cd ..
cd 7005
./bin/redis-server ./redis.conf
cd ..
cd 7006
./bin/redis-server ./redis.conf
cd ..
(10) 第十步:修改start-all.sh文件的权限
[root@localhost redis-cluster]# chmod u+x startall.sh
(11) 第十一步:启动所有的实例
[root@localhost redis-cluster]# ./startall.sh
(12) 第十二步:创建集群(关闭防火墙)
redis-cli --cluster create ip:port ip:port --cluster-replicas 1
[root@localhost redis_cluster]# cd /home/admin/myapps/redis-cluster/7001/bin
[root@localhost bin]# ./redis-cli --cluster create 192.168.197.132:7001 192.168.197.132:7002
192.168.197.132:7003 192.168.197.132:7004 192.168.197.132:7005 192.168.197.132:7006 --clusterreplicas 1
\>>> Creating cluster
Connecting to node 127.0.0.1:7001: OK
Connecting to node 127.0.0.1:7002: OK
Connecting to node 127.0.0.1:7003: OK
Connecting to node 127.0.0.1:7004: OK
Connecting to node 127.0.0.1:7005: OK
Connecting to node 127.0.0.1:7006: OK
\>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
127.0.0.1:7001
127.0.0.1:7002
127.0.0.1:7003
Adding replica 127.0.0.1:7004 to 127.0.0.1:7001
Adding replica 127.0.0.1:7005 to 127.0.0.1:7002
Adding replica 127.0.0.1:7006 to 127.0.0.1:7003
[OK] All 16384 slots covered.
13.4 连接集群
[root@localhost 7001]# ./bin/redis-cli -h 127.0.0.1 -p 7001 -c
[root@localhost 7001]# ./bin/redis-cli -h 127.0.0.1 -p 7001 -c
127.0.0.1:7001> set username java123
-> Redirected to slot [14315] located at 127.0.0.1:7003
OK
13.5 查看集群信息
127.0.0.1:7003> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_sent:1186
cluster_stats_messages_received:1186
13.6 查看集群中节点信息
127.0.0.1:7003> cluster nodes
713218b88321e5067fd8ad25c3bf7db88c878ccf 127.0.0.1:7003 myself,master - 0 0 3 connected 10923-
16383
e7fb45e74f828b53ccd8b335f3ed587aa115b903 127.0.0.1:7001 master - 0 1498877677276 1 connected 0-
5460
b1183545245b3a710a95d669d7bbcbb5e09896a0 127.0.0.1:7006 slave
713218b88321e5067fd8ad25c3bf7db88c878ccf 0 1498877679294 3 connected
8879c2ed9c141de70cb7d5fcb7d690ed8a200792 127.0.0.1:7005 slave
4a312b6fc90bfee187d43588ead99d83b407c892 0 1498877678285 5 connected
4a312b6fc90bfee187d43588ead99d83b407c892 127.0.0.1:7002 master - 0 1498877674248 2 connected
5461-10922
4f8c7455574e2f0aab1e2bb341eae319ac065039 127.0.0.1:7004 slave
e7fb45e74f828b53ccd8b335f3ed587aa115b903 0 1498877680308 4 connected
13.7 Jedis连接集群
13.7.1 关闭防火墙
13.7.2 代码实现
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
public static void main(String[] args) throws IOException {
// 创建一连接,JedisCluster对象,在系统中是单例存在
Set<HostAndPort> nodes = new HashSet<HostAndPort>();
nodes.add(new HostAndPort("192.168.197.132", 7001));
nodes.add(new HostAndPort("192.168.197.132", 7002));
nodes.add(new HostAndPort("192.168.197.132", 7003));
nodes.add(new HostAndPort("192.168.197.132", 7004));
nodes.add(new HostAndPort("192.168.197.132", 7005));
nodes.add(new HostAndPort("192.168.197.132", 7006));
JedisCluster cluster = new JedisCluster(nodes);
// 执行JedisCluster对象中的方法,方法和redis指令一一对应。
cluster.set("test1", "test111");
String result = cluster.get("test1");
System.out.println(result);
//存储List数据到列表中
cluster.lpush("site-list", "java");
cluster.lpush("site-list", "c");
cluster.lpush("site-list", "mysql");
// 获取存储的数据并输出
List<String> list = cluster.lrange("site-list", 0 ,2);
for(int i=0; i<list.size(); i++) {
System.out.println("列表项为: "+list.get(i));
}
// 程序结束时需要关闭JedisCluster对象
cluster.close();
System.out.println("集群测试成功!");
}
十四、Redis高端面试-缓存穿透,缓存击穿,缓存雪崩问题
14.1 缓存的概念
广义的缓存就是在第一次加载某些可能会复用数据的时候,在加载数据的同时,将数据放到一个指定的地点做保 存。再下次加载的时候,从这个指定地点去取数据。这里加缓存是有一个前提的,就是从这个地方取数据,比从数 据源取数据要快的多。
1. 虚拟机缓存(ehcache, JBoss Cache )2. 分布式缓存(redis, memcache )3. 数据库缓存
14.2 缓存雪崩
缓存雪崩通俗简单的理解就是:由于原有缓存失效(或者数据未加载到缓存中),新缓存未到期间(缓存正常从 Redis中获取,如下图)所有原本应该访问缓存的请求都去查询数据库了,而对数据库 CPU 和内存造成巨大压力, 严重的会造成数据库宕机,造成系统的崩溃。
缓存失效的时候如下图:
解决方案:
public Users getByUsers(Long id) {
// 1.先查询redis
String key = this.getClass().getName() + "-" +
Thread.currentThread().getStackTrace([1].getMethodName()+ "-id:" + id;
String userJson = redisService.getString(key);
if (!StringUtils.isEmpty(userJson)) {
Users users = JSONObject.parseObject(userJson, Users.class);
return users;
}
Users user = null;
try {
lock.lock();
// 查询db
user = userMapper.getUser(id);
redisService.setSet(key, JSONObject.toJSONString(user));
} catch (Exception e) {
} finally {
lock.unlock(); // 释放锁
}
return user;
}
14.3 缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找 不到,每次都要去数据库再查询一遍,然后返回空。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
解决方案 :1.如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问 数据库,这种办法最简单粗暴。2.把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,既可以避免当查询的值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key 进行预先校验,然后再放行给后面的正常缓存处理逻辑。
public String getByUsers2(Long id) {
// 1.先查询redis
String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()
[1].getMethodName()+ "-id:" + id;
String userName = redisService.getString(key);
if (!StringUtils.isEmpty(userName)) {
return userName;
}
System.out.println("######开始发送数据库DB请求########");
Users user = userMapper.getUser(id);
String value = null;
if (user == null) {
// 标识为null
value = "";
} else {
value = user.getName();
}
redisService.setString(key, value);
return value;
}
14.4 缓存击穿
对于一些设置了过期时间的 key ,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常 “ 热点 ” 的数据。 这个时候,需要考虑一个问题:缓存被“ 击穿 ” 的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,前者则是很多key 。
解决办法:①使用锁,单机用 synchronized,lock 等,分布式用分布式锁。②缓存过期时间不设置,而是设置在 key 对应的 value 里。如果检测到存的时间超过过期时间则异步更新缓存。
十五、Redis高端面试-分布式锁
15.1 使用分布式锁要满足的几个条件:
1. 系统是一个分布式系统(关键是分布式,单机的可以使用 ReentrantLock 或者 synchronized 代码块来实现)2. 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者 NoSQL )3. 同步访问(即有很多个进程同时访问同一个共享资源。)
15.2 什么是分布式锁?
15.3 应用的场景
线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决 这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。
15.4 使用redis的setNX命令实现分布式锁
15.4.1 实现的原理:
Redis 为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对 Redis 的连接并不存在竞争 关系。redis 的 SETNX 命令可以方便的实现分布式锁。
15.4.2 基本命令解析
语法:SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。SETNX 是『SET if Not eXists』 ( 如果不存在,则 SET) 的简写
设置成功,返回 1 。设置失败,返回 0 。
redis> EXISTS job # job 不存在
(integer) 0
redis> SETNX job "programmer" # job 设置成功
(integer) 1
redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(integer) 0
redis> GET job # 没有被覆盖
"programmer"
SETNX lock.foo <current Unix time + lock timeout + 1>
(2) getSET
GETSET key value
返回给定 key 的旧值 [ 之前的值 ] 。当 key 没有旧值时,也即是, key 不存在时,返回 nil 。
1 、同一时刻只能有一个进程获取到锁。 setnx2 、释放锁:锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;(最简单的方式就是 del , 如果在删除之前死锁了。)
ex:
53秒设置--58秒到期
当前时间为56秒,没有过期
当前时间为 59 秒,过期 . (当前时间大于设置的时间)
上面的锁定逻辑有一个问题: 如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决 ?我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于 lock.foo 的值,说明该锁 已失效,可以被重新使用。发生这种情况时,可不能简单的通过 DEL 来删除锁,然后再 SETNX 一次(讲道理, 删除锁的操作应该是锁拥有 者执行的,这里只需要等它超时即可 ),当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞 态条件, 让我们模拟一下这个场景:C0 操作超时了,但它还持有着锁, C1 和 C2 读取 lock.foo 检查时间戳,先后发现超时了。 C1 发送 DEL lock.foo C1 发送SETNX lock.foo 并且成功了。 C2 发送 DEL lock.foo C2 发送 SETNX lock.foo 并且成功了。 这样一来, C1 , C2 都拿到了锁!问题大了!幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:C3发送 SETNX lock.foo 想要获得锁,由于 C0 还持有锁,所以 Redis 返回给 C3 一个 0 C3 发送 GET lock.foo 以检查锁 是否超时了,如果没超时,则等待或重试。 反之,如果已超时,C3 通过下面的操作来尝试获得锁: GETSET lock.foo 通过 GETSET, C3 拿到的时间戳如果仍然是超时的,那就说明, C3 如愿以偿拿到锁了。 如果在 C3 之前,有个叫 C4 的客 户端比C3 快一步执行了上面的操作,那么 C3 拿到的时间戳是个未超时的值,这时, C3 没有如期获得锁,需要再次 等待或重试。留意一下,尽管C3 没拿到锁,但它改写了 C4 设置的锁的超时值,不过这一点非常微小的误差带来的 影响可以忽略不计。
注意 :为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时, 再去做DEL 操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就 不必解锁了。
public static boolean lock(String lockName) {
Jedis jedis = RedisPool.getJedis();
//lockName可以为共享变量名,也可以为方法名,主要是用于模拟锁信息
System.out.println(Thread.currentThread() + "开始尝试加锁!");
Long result = jedis.setnx(lockName, String.valueOf(System.currentTimeMillis() + 5000));
if (result != null && result.intValue() == 1){
System.out.println(Thread.currentThread() + "加锁成功!");
jedis.expire(lockName, 5);
System.out.println(Thread.currentThread() + "执行业务逻辑!");
jedis.del(lockName);
return true;
} else {//判断是否死锁
String lockValueA = jedis.get(lockName);
//得到锁的过期时间,判断小于当前时间,说明已超时但是没释放锁,通过下面的操作来尝试获得锁。下面逻辑防止死锁[已经过期但是没有释放锁的情况]
if (lockValueA != null && Long.parseLong(lockValueA) <
System.currentTimeMillis()){
String lockValueB = jedis.getSet(lockName,
String.valueOf(System.currentTimeMillis() + 5000));
//这里返回的值是旧值,如果有的话。之前没有值就返回null,设置的是新超时。
if (lockValueB == null || lockValueB.equals(lockValueA)){
System.out.println(Thread.currentThread() + "加锁成功!");
jedis.expire(lockName, 5);
System.out.println(Thread.currentThread() + "执行业务逻辑!");
jedis.del(lockName);
return true;
} else {
return false;
}
} else {
return false;
}
}
}