目录

​​一,redis介绍​​

​​1,什么是NoSQL​​

​​2,为什么要使用NoSQL​​

​​3,常见的NoSQL产品​​

​​4,各产品的介绍​​

​​5,什么是Redis​​

​​5.1 Redis介绍​​

​​5.2 Redis的优势​​

​​5.3 Redis应用场景​​

​​二,redis安装与启动​​

​​1,环境准备​​

​​2,Redis安装​​

​​2.1 Redis的编译环境​​

​​2.2 Redis的安装​​

​​2.3 Redis启动​​

​​三,Redis客户端访问以及停止服务​​

​​1,客户端访问Redis​​

​​1.1  向Redis服务器发送命令​​

​​1.2 退出客户端​​

​​2,Redis的停止​​

​​3,第三方工具(redis-desktop-manager)操作Redis​​

​​四,redis数据类型介绍​​

​​五,redis常用指令操作数据库​​

​​1,String 类型​​

​​1.1 语法​​

​​1.2 举例​​

​​2, 字符串数字的递增与递减​​

​​2.1 语法​​

​​2.2 举例​​

​​3,Hash散列(了解)​​

​​3.1 语法​​

​​3.2 举例​​

​​4,队列List​​

​​4.1 语法​​

​​4.2 举例​​

​​5,Set集合​​

​​5.1 语法​​

​​5.2 举例​​

​​6,Zset有序集合(了解)​​

​​6.1 语法​​

​​6.2 举例​​

​​7,HyoperLogLog命令​​

​​7.1   基本命令​​

​​7.2 举例​​

​​8,其他命令​​

​​9,Redis的多数据库​​

​​六,redis事务管理​​

​​1,介绍​​

​​2,举例​​

​​七,redis发布订阅模式​​

​​1,介绍​​

​​2,举例​​

​​八,jedis访问redis​​

​​1,在Java的Maven项目中引入依赖​​

​​2,连接服务器​​

​​2.1 单实例连接​​

​​2.3 连接池​​

​​九,redis持久化方案​​

​​1,RDB​​

​​2,AOF​​

​​3,AOF与RDB区别​​

​​十,redis主从复制的实现​​

​​1,主从复制介绍​​

​​2,主从搭建步骤​​

​​3,复制的过程原理​​

​​4,复制架构中出现宕机情况?​​

​​十一,哨兵模式​​

​​1,介绍​​

​​2,配置哨兵模式​​

​​十二,redis集群​​

​​1, redis-cluster架构​​

​​2,redis-cluster投票:容错​​

​​3,redis集群搭建步骤​​

​​3.1 集群搭建​​

​​3.2 连接集群​​

​​3.3 查看集群信息​​

​​4,Jedis连接redis集群​​

​​十三,缓存雪崩、缓存穿透和缓存击穿​​

​​1,缓存的概念​​

​​2,缓存雪崩​​

​​解决方案:​​

​​3,缓存穿透​​

​​4,缓存击穿​​

​​十五,redis分布式锁​​

​​1,使用分布式锁要满足的几个条件​​

​​2,什么是分布式锁?​​

​​3,应用的场景​​

​​4,使用redis的setNX命令实现分布式锁​​

​​4.1 原理​​

​​4.2 命令解析​​

​​4.3 解决死锁​​


一,redis介绍

1,什么是NoSQL

NoSQL,泛指非关系型的数据库。

随着互联网web2.0网站的兴起,传统的关系数据库在处理web2.0网站,特别是 超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,出现了很多难以克服的问题,而非关系型的 数据库则由于其本身的特点得到了非常迅速的发展。

NoSQL数据库的产生就是为了解决大规模数据集合,多重数据种类带来的挑战,尤其是大数据应用难题。

NoSQL最常见的解释是“non-relational”, “Not Only SQL”也被很多人接受。NoSQL仅仅是一个概念,泛指非关系 型的数据库,区别于关系数据库,它们不保证关系数据的ACID特性。

2,为什么要使用NoSQL

传统的数据库遇到的瓶颈

传统的关系数据库具有不错的性能,高稳定型,久经历史考验,而且使用简单,功能强大,同时也积累 了大量的 成功案例。在互联网领域,MySQL成为了绝对靠前的王者,毫不夸张的说,MySQL为互联网 的发展做出了卓越的 贡献。

在90年代,一个网站的访问量一般都不大,用单个数据库完全可以轻松应付。在那个时候,更多的都是 静态 网页,动态交互类型的网站不多。

到了最近10年,网站开始快速发展。火爆的论坛、博客、sns、微博逐渐引领web领域的潮流。在初 期,论坛的流 量其实也不大,如果你接触网络比较早,你可能还记得那个时候还有文本型存储的论坛程 序,可以想象一般的论坛 的流量有多大。

现在网站的特点:

(1) 高并发读写。Web2.0网站,数据库并发负载非常高,往往达到每秒上万次的读写请求。

(2) 高容量存储和高效存储。Web2.0网站通常需要在后台数据库中存储海量数据,如何存储海量数据并进行高效的查询往往是一个挑战。

(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模型,通过复制模型也能实现高可用。

3,常见的NoSQL产品

10-NoSQL_Redis_开课吧


4,各产品的介绍

分类

相关产品

应用场景

数据模型

优点

缺点

键值数据库

Redis、Memcached、Riak

内容缓存,如会话、配置文件、参数等;

频繁读写、拥有简单数据模型的应用;

<key, value>键值对,通过散列表实现

扩展性号,灵活性好,大量数据操作时性能高

数据无结构化,通常之被当作字符串或者二进制数据;

只能通过键来查询值;

列族数据库

Bigable、HBase、Cassandra

分布式数据存储与管理

以列族式存储,将同一列数据存放在一起

可扩展性强,查找速度快,复杂性低

功能局限,不支持事务的强一致性;

文档数据库

MongoDB、CouchDB

Web应用,存储面向文档或类似半结构化的数据

<key, value>value为JSON结构的文档

数据结构灵活,可以根据value构建索引

缺乏统一查询语法

图形数据库

Neo4j、InfoGrid

社交网络、推荐系统、专注构建关系图谱

图结构

支持复杂的图形算法

复杂性高,只能支持一定 的数据规模

5,什么是Redis

5.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(主从)模式的数据备份;

5.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;

5.3 Redis应用场景

(1) 缓存(数据查询,短连接,新闻内容,商品内容等),使用最多

(2) 聊天室在线好友列表

(3) 任务队列(秒杀,抢购,12306等)

(4) 应用排行榜

(5) 网站访问统计

(6) 数据过期处理(可以精确到毫秒)

(7) 分布式集群架构中的session问题

Redis下载

​Redis​​ 英文地址

​redis中文官方网站​​ 中文地址

二,redis安装与启动

1,环境准备

(1)虚拟机版本:VMware® Workstation 12 Pro

(2) Linux系统:Centos Release 6.5(个人用的是Centos Release 7.2)

(3) 远程命令端:xshell

(4)文件传输工具:SecureFXPortable

2,Redis安装

2.1 Redis的编译环境

Redis是C语言开发的,安装redis需要先去官网下载源码进行编译,编译需要依赖于GCC编译环境,如果CentOS上 没有安装gcc编译环境,需要提前安装,安装命令如下:(推荐使用root用户处理这些操作,我这里用的是个人用户,部分命令前需要加sudo)


[root@localhost ~]# yum install gcc-c++


都选y即可

2.2 Redis的安装

(1) 使用SecureFXPortable上传Redis安装文件到Linux目录

10-NoSQL_Redis_开课吧_02


(2)上传Redis安装文件,这里我上传自建文件夹: /home/xrh/MySoftware

10-NoSQL_Redis_分布式锁_03


(3)解压redis文件


[root@localhost local]# tar -zxvf redis-5.0.5.tar.gz


10-NoSQL_Redis_redis_04


(4)编译Redis(编译,将.c文件编译为.o文件) 

进入解压文件夹,执行 make 


[root@localhost local]# cd redis-5.0.5
[root@localhost redis-5.0.5]# make


10-NoSQL_Redis_缓存_05


编译成功!如果编译过程中出错,先删除安装文件目录,后解压重新编译。(如果前面配置顺利的话,这里一般不会出问题的) 

(5)安装

说明:这里的/home/admin/myapps/redis 是自定义的redis安装路径


[root@localhost redis-5.0.5]# make PREFIX=/home/admin/myapps/redis install


10-NoSQL_Redis_集群_06


(6)安装之后的bin目录

bin文件夹下的命令:

10-NoSQL_Redis_开课吧_07


  • redis-benchmark         ---- 性能测试工具
  • redis-check-aof           ---- AOF文件修复工具
  • redis-check-dump       ---- RDB文件检查工具(快照持久化文件)
  • redig-cli                       ---- 命令行客户端
  • redis-server                 ---- redis 服务器启动命令

(7) Copy文件 

Redis启动需要一个配置文件,可以修改端口号信息。将redis解压的文件夹中的redis.conf文件复制到安装目录


[root@localhost redis-5.0.5]# cp redis.conf /home/admin/myapps/redis


10-NoSQL_Redis_缓存_08


2.3 Redis启动

1,Redis的前端模式启动

直接运行bin/redis-server将使用前端模式启动,前端模式启动的缺点是启动完成后,不能再进行其他操作,如果要操作必须使用ctrl+c,同时redis-server程序结束,不推荐此方法。


[root@localhost bin]# ./redis-server


10-NoSQL_Redis_分布式锁_09


使用ctrl+c退出前端启动。

2,Redis的后端启动 

修改redis.conf配置文件,设置:daemonize yes,然后可以使用后端模式启动。


[root@localhost redis]# vi redis.conf


10-NoSQL_Redis_集群_10


启动时,指定配置文件(这里所在文件夹是redis)


[root@localhost redis]# ./bin/redis-server ./redis.conf


10-NoSQL_Redis_缓存_11


Redis默认端口:6379,通过当前服务进行查看


[root@localhost redis]# ps -ef | grep -i redis


10-NoSQL_Redis_开课吧_12


三,Redis客户端访问以及停止服务

1,客户端访问Redis

首先要启动Redis。跳转到安装目录的redis文件夹中,通过后端方式运行启动命令

10-NoSQL_Redis_开课吧_13


如果想要通过指令来操作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 端口号


1.1  向Redis服务器发送命令

Ping,测试客户端与Redis的连接是否正常,如果连接正常,回收到pong


127.0.0.1:6379> ping 
PONG


1.2 退出客户端


127.0.0.1:6379> quit


2,Redis的停止

(1) 强制结束程序。强制终止Redis进程可能会导致redis持久化数据丢失。

语法:kill -9 pid


kill -9 31475 # pid需要通过"ps aux | grep -i redis"进行查询


10-NoSQL_Redis_redis_14


(2) 正确停止Redis的方式应该是向Redis发送SHUTDOWN命令,方法为(关闭默认的端口)


[root@localhost redis]# ./bin/redis-cli shutdown


10-NoSQL_Redis_集群_15


3,第三方工具(redis-desktop-manager)操作Redis

这里可以通过Windows中安装的RDM(redis-desktop-manager)查看Redis数据库中存储的数据,类似于之前使用的Navicate。

10-NoSQL_Redis_缓存_16


注意:需要关闭linux防火墙并且修改redis.conf文件中的bind参数


bind linux的ip地址


10-NoSQL_Redis_分布式锁_17


此时如果通过redis客户端访问的时候,代码如下:


./redis-cli -h 192.168.197.132 -p 6379


如果连接不到虚拟机中的redis server可以参考这里的方法,亲测有效。

​​@江河涌流【解决Redis Desktop Manager(RDM)无法连接redis虚拟机问题】​​

由于我的主机和虚拟机之间的网络连接没有问题,所以直接从【4.修改redis配置文件】开始配置

10-NoSQL_Redis_缓存_18

其中注释bind的做法有风险,需要注意一下:

10-NoSQL_Redis_开课吧_19


四,redis数据类型介绍

Redis是一种基于内存的数据库,并且提供一定的持久化功能,它是一种键值(key-value)数据库,使用 key 作为 索引找到当前缓存的数据,并且返回给程序调用者。

当前的 Redis 支持 6 种数据类型,它们分别是

  • 字符串:String
  • 列表:List
  • 集合:set
  • 哈希结构 :hash
  • 有序集合:zset
  • 基数:HyperLogLog

数据类型

数据类型存储的值

说明

字符串:String

可以是保存字符串、整数和浮点数

可以对字符串进行操作,比如增加字符或者求子串;

如果是整数或者浮点数,可以实现计算,比如自增等;

列表:List

它是一个链表,它的每一个节点都包含一个字符串

Redis支持从链表的两端插入或者弹出节点,或者通过偏移对它进行裁剪;

还可以读取一个或者多个节点,根据条件删除或者查找节点等;

集合:set

它是一个收集器,但是是无序的,在它里而每一个元素都是一个字符串,而且是独一无二,各不相同的

可以新增、读取、删除单个元素:检测一个元素是否在集合中;

计算它和其他集合的交集、并集和差集等;

随机从集合中读取元素;

哈希结构 :hash

它类似于Java语言中的Map,是一个键值对应的无序列表

可以增、删、查、改单个键值对,也可以获取所有的键值对

有序集合:zset

它是一个有序的集合,可以包含字符串、整数、浮点数、分值(score) , 元素的排序是依据分值的大小来决定的

可以增、删、查、改元素,根据分值的范围或者成员來获取对应的元索

基数:HyperLogLog

它的作用是计算重复的值,以确定存储的数量

只提供基数的运算,不提供返回的功能

五,redis常用指令操作数据库

命令学习网站​​Redis 命令参考 — Redis 命令参考​

需要先将redis启动,关闭时记得按照推荐的方式(./bin/redis-cli shutdown)关闭

1,String 类型

1.1 语法

1,单个赋值:


SET key value


2,多键赋值:


MSET key value [key value …]


3,单个取值:


GET key


4,多键取值:


MGET key [key …]


5,删除:


DEL key


1.2 举例


[root@localhost redis]# ./bin/redis-cli 
127.0.0.1:6379> set k1 zhangsan
OK
127.0.0.1:6379> get k1
"zhangsan"
127.0.0.1:6379> mset k2 lisi k3 wangwu
OK
127.0.0.1:6379> mget k1 k2 k3
1) "zhangsan"
2) "lisi"
3) "wangwu"
127.0.0.1:6379> del k3
(integer) 1
127.0.0.1:6379> get k3
(nil)
127.0.0.1:6379>


2, 字符串数字的递增与递减

当存储的字符串是整数时,Redis提供了实用的命令可以让当前键值递增/减,并返回递增/减后的值。

2.1 语法

1,递增数字: 


INCR key


2,递减数值: 


DECR key


3,增加指定的整数:


INCRBY key increment


4,减少指定的整数:


DECRBY key decrement


2.2 举例


127.0.0.1:6379> incr num
(integer) 1
127.0.0.1:6379> incr num
(integer) 2
127.0.0.1:6379> decr num
(integer) 1
127.0.0.1:6379> decr num
(integer) 0
127.0.0.1:6379> incrby num2 2
(integer) 2
127.0.0.1:6379> incrby num2 3
(integer) 5
127.0.0.1:6379> decrby num2 1
(integer) 4
127.0.0.1:6379> decrby num2 1
(integer) 3
127.0.0.1:6379> mget num num2
1) "0"
2) "3"
127.0.0.1:6379>


10-NoSQL_Redis_缓存_20


3,Hash散列(了解)

hash叫散列类型,它提供了字段和字段值的映射。字段值只能是字符串类型,不支持散列类型、集合类型等其它类型。相当于是对象格式的存储。

3.1 语法

1,单字段赋值:(HSET命令不区分插入和更新操作,当执行插入操作时HSET命令返回1,当执行更新操作时 返回0)


HSET key field value


2,单字段取值:


HGET key field


3,多字段赋值:


HMSET key field value [field value ...]


4,多字段取值:


HMGET key field [field ...]


5,获取所有字段的值:


HGETALL key


6,删除字段:   


HDEL key field [field ...]


3.2 举例


127.0.0.1:6379> hset user1 username ganyu
(integer) 1
127.0.0.1:6379> hset user1 username shenhe
(integer) 0
127.0.0.1:6379> hget user1 shenhe
(nil)
127.0.0.1:6379> hget user1 username
"shenhe"
127.0.0.1:6379> hmset user1 password 123 age 18
OK
127.0.0.1:6379> hmget user1 password age
1) "123"
2) "18"
127.0.0.1:6379> hmget user1 username password age
1) "shenhe"
2) "123"
3) "18"
127.0.0.1:6379> hgetall user1
1) "username"
2) "shenhe"
3) "password"
4) "123"
5) "age"
6) "18"
127.0.0.1:6379> hdel user1 password
(integer) 1
127.0.0.1:6379> hgetall user1
1) "username"
2) "shenhe"
3) "age"
4) "18"
127.0.0.1:6379>


4,队列List

Redis的list是采用来链表来存储,双向链表存储数据,特点:增删快、查询慢(Linkedlist)。这个队列是有序的。

4.1 语法

1,向列表左边增加元素:


LPUSH key value [value ...]


2,从列表左边弹出元素:(临时存储,弹出后,从队列中清除)


LPOP key


3,向列表右边增加元素:


RPUSH key value [value ...]


4,从列表右边弹出元素:


RPOP key


5,获取列表元素的个数:


LLEN key


6,查看列表:(将返回 [start, stop]


LRANGE key start stop


4.2 举例

列表左侧增加、弹出


127.0.0.1:6379> lpush list1 a1 a2 3
(integer) 3
127.0.0.1:6379> lpop list1
"3"
127.0.0.1:6379> lpop list1
"a2"
127.0.0.1:6379>


列表右侧增加、弹出 


127.0.0.1:6379> rpush list2 1 2 3
(integer) 3
127.0.0.1:6379> rpop list2
"3"
127.0.0.1:6379> rpop list2
"2"
127.0.0.1:6379>


获取列表长度


127.0.0.1:6379> llen list1
(integer) 1


查看列表


127.0.0.1:6379> lrange list2 0 3
1) "1"
127.0.0.1:6379>


5,Set集合

5.1 语法

1,增加元素语法:


SADD key member [member ...]


2,删除元素语法:


SREM key member [member ...]


3,获得集合所有元素:


SMEMBERS key


4,判断元素是否在集合中:


SISMEMBER key member


5.2 举例


127.0.0.1:6379> sadd set1 u1
(integer) 1
127.0.0.1:6379> sadd set1 u2
(integer) 1
127.0.0.1:6379> sadd set1 u3
(integer) 1
127.0.0.1:6379> smembers set1
1) "u3"
2) "u2"
3) "u1"
127.0.0.1:6379> srem set1 u2
(integer) 1
127.0.0.1:6379> smembers set1
1) "u3"
2) "u1"
127.0.0.1:6379> sismember set1 u
(integer) 0
127.0.0.1:6379> sismember set1 u1
(integer) 1
127.0.0.1:6379>


6,Zset有序集合(了解)

Sortedset又叫zset,是有序集合,可排序的,但是唯一。 Sortedset和set的不同之处,是会给set中的元素添加一个分数,然后通过这个分数进行排序。

6.1 语法

1,增加元素:(向有序集合中加入一个元素和该元素的分数(score),如果该元素已经存在则会用新的分数替换原有的分数)     


ZADD key score member [score member ...]


2,获得部分元素列表:(获得排名在某个范围的元素列表,并按照元素分数降序返回 )


ZREVRANGE key start stop [WITHSCORES]


3,获取元素的分数:     


ZSCORE key member


4,删除元素:(获得元素的分数的可以在命令尾部加上WITHSCORES参数)


ZREM key member [member ...]


5,给某一个属性加分数或减分:(减分时使用负数)


ZINCRBY key increment member


6.2 举例


127.0.0.1:6379> zadd set2 10 u1 20 u2 30 u3 40 u4
(integer) 4
127.0.0.1:6379> zrevrange set2 0 3
1) "u4"
2) "u3"
3) "u2"
4) "u1"
127.0.0.1:6379> zrevrange set2 0 3 withscores
1) "u4"
2) "40"
3) "u3"
4) "30"
5) "u2"
6) "20"
7) "u1"
8) "10"
127.0.0.1:6379> zscore set2 u3
"30"
127.0.0.1:6379> zrem set2 u1
(integer) 1
127.0.0.1:6379> zrevrange set2 0 3 withscores
1) "u4"
2) "40"
3) "u3"
4) "30"
5) "u2"
6) "20"
127.0.0.1:6379>


给属性加减分


127.0.0.1:6379> zincrby set2 5 u3
"35"
127.0.0.1:6379> zrevrange set2 0 3 withscores
1) "u4"
2) "40"
3) "u3"
4) "35"
5) "u2"
6) "20"
127.0.0.1:6379>


其他示例

10-NoSQL_Redis_缓存_21


7,HyoperLogLog命令

HyperLogLog是一种使用随机化的算法,以少量内存提供集合中唯一元素数量的近似值。 

HyperLogLog 可以接受多个元素作为输入,并给出输入元素的基数估算值:

  • 基数:集合中不同元素的数量。比如 {‘apple’, ‘banana’, ‘cherry’, ‘banana’, ‘apple’} 的基数就是 3 。 
  • 估算值:算法给出的基数并不是精确的,可能会比实际稍微多一些或者稍微少一些,但会控制在合理的范围之内。

HyperLogLog 的优点是,即使输入元素的数量或者体积非常非常大,计算基数所需的空间总是固定的、并且是很 小的。 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。 

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

7.1   基本命令

10-NoSQL_Redis_集群_22


7.2 举例


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


8,其他命令

1,keys返回满足给定pattern 的所有key


keys user* //查询以user开头的key 
keys * //查询所有的key


2,exists确认一个key 是否存在,存在返回1


127.0.0.1:6379> exists num2 //语法:exists key 
(integer) 1
127.0.0.1:6379> exists num23
(integer) 0


3,del删除一个key


127.0.0.1:6379> del num1 //语法: del key 删除存在的key返回1,不存在的key返回0 
(integer) 1
127.0.0.1:6379> del num23
(integer) 0


4,rename重命名key:rename oldkey newkey


127.0.0.1:6379> rename k1 k11 
OK
127.0.0.1:6379> keys *
1) "ulist"
2) "k2"
3) "user1"
4) "num2"
5) "clist"
6) "k11"


5,type返回值的类型: type key


127.0.0.1:6379> type ulist 
set
127.0.0.1:6379> type k11
string
127.0.0.1:6379> type alist
list


6,设置key的生存时间:缓存的数据一般都是需要设置生存时间的,即:到期后数据销毁。

设置key的生存时间(单位:秒)key在多少秒后会自动删除


EXPIRE key seconds


查看key剩余的生存时间


TTL key


清除生存时间


PERSIST key


127.0.0.1:6379> set a1 123 
OK
127.0.0.1:6379> get a1
"123"
127.0.0.1:6379> expire a1 60
(integer) 1
127.0.0.1:6379> ttl a1
(integer) 56
127.0.0.1:6379> ttl a1
(integer) 51
127.0.0.1:6379> ttl a1
(integer) 47


7,获取服务器信息和统计:info

8,删除当前选择数据库中的所有key:flflushdb

9,删除所有数据库中的所有key:flflushall

9,Redis的多数据库

一个redis实例key包括多个数据库,客户端可以指定连接某个redis实例的哪个数据库,就好比一个mysql中创建多 个数据库,客户端连接时指定连接哪个数据库。

一个redis实例最多可提供16个数据库,下标从0-15,客户端默认连接第0号数据库,也可以通过select选择连接哪 个数据库。

连接1号库:

10-NoSQL_Redis_分布式锁_23


切换至0数据库下面

10-NoSQL_Redis_开课吧_24


将key的数据移动到1号数据库: move key 数据库编号

10-NoSQL_Redis_分布式锁_25


六,redis事务管理

1,介绍

Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:

  • 事务是一个隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客 户端发送来的命令请求所打断。
  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务。
  • 命令入队。
  • 执行事务。

2,举例

以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事 务, 一并执行事务中的所有命令:


127.0.0.1:6379> multi 
OK
127.0.0.1:6379> set bookname java
QUEUED
127.0.0.1:6379> set bookname c++
QUEUED
127.0.0.1:6379> set bookname html
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) OK


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++"


七,redis发布订阅模式

1,介绍

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

Redis 客户端可以订阅任意数量的频道。

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

10-NoSQL_Redis_缓存_26


当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

10-NoSQL_Redis_开课吧_27


2,举例

在我们实例中我们创建了订阅频道名为 redisMessage:


127.0.0.1:6379> subscribe redisMessage 
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisMessage"
3) (integer) 1


现在,我们先重新开启个 redis 客户端,然后在同一个频道 redisMessage 发布两次消息,订阅者就能接收到消息。


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

通过Java访问redis。

1,在Java的Maven项目中引入依赖


<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
</dependency>


记得

10-NoSQL_Redis_开课吧_28


PS:确认远程服务器是否可以ping通: ping vm的ip地址

10-NoSQL_Redis_开课吧_29

​PS:确认防火墙是否关闭或放行


service iptables stop service iptables status


2,连接服务器

2.1 单实例连接


Jedis jedis = new Jedis(“ip地址”, 端口号);//建立链接


public class App 
{
public static void main( String[] args )
{
Jedis jedis = new Jedis("192.168.19.128", 6379);
jedis.set("role01", "甘雨");
String role01 = jedis.get("role01");
System.out.println(role01);
}
}


10-NoSQL_Redis_分布式锁_30


10-NoSQL_Redis_缓存_31


踩坑指南:

1,【SocketTimeoutException 连接超时】和【Connection refused 拒绝访问】两种常见问题

  • 【SocketTimeoutException 连接超时】:大部分情况是防火墙没有关闭;
  • 【Connection refused 拒绝访问】:redis.conf中bind为127.0.0.1,将其修改为虚拟机ip地址;
  • 修改配置后记得重启redis!!!

详细的解决方法可以参考这里​​@ 宜春【Jedis 连接 Redis报JedisConnectionException: java.net.ConnectException: Connection refused】​​

2,如何关闭CentOS防火墙


# CentOS7systemctl stop firewalld.service(关闭防火墙)systemctl disable firewalld.service(关闭防火墙自动启动)


详细可以看这里的介绍​​@JierenDong【CentOS如何关闭防火墙服务】​​

2.3 连接池


public class App
{
public static void main( String[] args )
{
// 1. 获取连接池配置对象,设置配置项
JedisPoolConfig config = new JedisPoolConfig();
// 1.1 最大的连接数
config.setMaxTotal(30);
// 2. 获取连接池
JedisPool jedisPool = new JedisPool(config, "192.168.19.128", 6379);
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
// 3. 设置数据
jedis.set("role01", "申鹤");
String name = jedis.get("role01");
System.out.println(name);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
// 4. 释放资源
if (jedisPool != null) {
jedisPool.close();
}
}
}
}


10-NoSQL_Redis_分布式锁_32


10-NoSQL_Redis_redis_33


九,redis持久化方案

由于redis的值放在内存中,为防止突然断电等特殊情况的发生,需要对数据进行持久化备份。即将内存数据保存到硬盘。

1,RDB

RDB 是以二进制文件,是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化 的文件,达到数据恢复。

  • 优点:使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
  • 缺点:RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合 数据要求不严谨的时候

这里说的这个执行数据写入到临时文件的时间点是可以通过配置来自己确定的,通过配置redis 在 n 秒内如果超过 m 个 key 被修改则执行一次 RDB 操作。这个操作就类似于在这个时间点来保存一次 Redis 的所有数据,一次快照 数据。所有这个持久化方法也通常叫做 snapshots。

RDB 默认开启,redis.conf 中的具体配置参数如下;


#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


注意:测试时使用root用户操作

10-NoSQL_Redis_缓存_34


2,AOF

Append-Only File,将“操作 + 数据”以格式化指令的方式追加到操作日志文件的尾部,在 append 操作返回后(已经写入到文件或者将要写入),才进行实际的数据变更,“日志文件”保存了历史所有的操作过程。当 server 需要数据恢复时,可以直接 replay 此日志文件,即可还原所有的操作过程。AOF 相对可靠,AOF 文件内容是字符串,非常 容易阅读和解析。

优点:

  • 可以保持更高的数据完整性,如果设置追加 fifile 的时间是 1s,如果 redis 发生故障,最多会丢失 1s 的数 据;且如果日志写入不完整,支持 redis-check-aof 来进行日志修复;
  • AOF 文件没被 rewrite 之前(文件过大时会对 命令进行合并重写),可以删除其中的某些命令(比如误操作的 flflushall);
  • 容易阅读和解析;

缺点:

  • AOF 文件比 RDB 文件大,且恢复速度慢。

我们可以简单的认为 AOF 就是日志文件,此文件只会记录“变更操作”(例如:set/del 等),如果 server 中持续的大量变更操作,将会导致 AOF 文件非常的庞大,意味着 server 失效后,数据恢复的过程将会很长;

事实上,一条数据经过多次变更,将会产生多条 AOF 记录,其实只要保存当前的状态,历史的操作记录是可以抛弃的;因为 AOF 持久化模式还伴生了“AOF rewrite”。

  • AOF 的特性决定了它相对比较安全,如果你期望数据更少的丢失,那么可以采用 AOF 模式。
  • 如果 AOF 文件正在被写入时突然 server 失效,有可能导致文件的最后一次记录是不完整,你可以通过手工或者程序的方式去检测并修 正不完整的记录,以便通过 aof 文件恢复能够正常;
  • 同时需要提醒,如果你的 redis 持久化手段中有 aof,那么在 server 故障失效后再次启动前,需要检测 aof 文件的完整性。

AOF 默认关闭,开启方法,修改配置文件 reds.conf:appendonly yes


##此选项为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


AOF 是文件操作,对于变更操作比较密集的 server,那么必将造成磁盘 IO 的负荷加重;

此外 linux 对文件操作采 取了“延迟写入”手段,即并非每次 write 操作都会触发实际磁盘操作,而是进入了 buffer 中,当 buffer 数据达到阀值时触发实际写入(也有其他时机),这是 linux 对文件系统的优化,但是这却有可能带来隐患,如果 buffer 没有 刷新到磁盘,此时物理机器失效(比如断电),那么有可能导致最后一条或者多条 aof 记录的丢失。

通过上述配置文 件,可以得知 redis 提供了 3 中 aof 记录同步选项:

  • always:每一条 aof 记录都立即同步到文件,这是最安全的方式,也以为更多的磁盘操作和阻塞延迟,是 IO 开支 较大。
  • everysec:每秒同步一次,性能和安全都比较中庸的方式,也是 redis 推荐的方式。如果遇到物理服务器故障,有可能导致最近一秒内 aof 记录丢失(可能为部分丢失)。
  • no:redis 并不直接调用文件同步,而是交给操作系统来处理,操作系统可以根据 buffer 填充情况 / 通道空闲时间 等择机触发同步;这是一种普通的文件操作方式。性能较好,在物理服务器故障时,数据丢失量会因 OS 配置有关。

其实,我们可以选择的太少,everysec 是最佳的选择。如果你非常在意每个数据都极其可靠,建议你选择一款“关系型数据库”。

3,AOF与RDB区别

RDB:

RDB是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

优点:

  • 使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能

缺点:

  • RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。所以这种方式更适合 数据要求不严谨的时候

AOF:

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主从复制的实现

1,主从复制介绍

  • 持久化保证了即使redis服务重启也不会丢失数据,但是当redis服务器的硬盘损坏了可能会导致数据丢失,通过redis的主从复制机制就可以避免这种单点故障(单台服务器的故障)。
  • 主redis中的数据和从上的数据保持实时同步,当主redis写入数据时通过主从复制机制复制到两个从服务上。
  • 主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求.
  • 主机master配置:无需配置

10-NoSQL_Redis_缓存_35


工作中一般选用:一主两从或一主一从

数据会同步到从服务器。在这个集群中的几台服务器上都有同样的数据。

2,主从搭建步骤

主机:不用配置。仅仅只需要配置从机,从机slave配置:(这里是伪集群)

1,复制出一个从机,注意使用root用户


[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


2,修改从机的redis.conf(这里是核心配置步骤,配置主机地址)

检索文件: 输入:/replicaof 当前页没有时,输入n,查找下一页。

10-NoSQL_Redis_redis_36


3,修改从机的port地址为6380(区别于主节点的6379)

在从机redis.conf中修改

10-NoSQL_Redis_分布式锁_37


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>


查看当前为主机还是从机(输入info replication)

10-NoSQL_Redis_分布式锁_38

​ 

10-NoSQL_Redis_redis_39


停止客户端:


./bin/redis-cli -p 6380 shutdown


主机一旦发生增删改操作,那么从机会自动将数据同步到从机中;

从机不能执行写操作,只能读:


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.


3,复制的过程原理

当从库和主库建立MS(master slaver)关系后,会向主数据库发送SYNC命令;

主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来;

快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis;

从Redis接收到后,会载入快照文件并且执行收到的缓存命令;

主Redis每当接收到写命令时就会将命令发送从Redis,保证数据的一致;【内部完成,所以不支持客户端在从机人为写数据。】

4,复制架构中出现宕机情况?

从Redis宕机:重启就好

主Redis宕机:从数据库(从机)中执行SLAVEOF NO ONE命令,断开主从关系并且提升为主库继续服务(把一个从做为主机,这个时候新主机[之前的从机]就具备写入的能力);主服务器修好后,重新启动后,执行SLAVEOF命令,将其设置为从库[老主机设置为从机]。[手动执行,过程复杂,容易出错。]

是否有更好的方案?

那就是哨兵模式

十一,哨兵模式

1,介绍

哨兵模式:给集群分配一个站岗的。

哨兵的作用就是对Redis系统的运行情况监控,它是一个独立进程,它的功能:

  1. 监控主数据库和从数据库是否运行正常;
  2. 主数据出现故障后自动将从数据库转化为主数据库;

如果主机宕机,开启选举工作,选择一个从做主机。

环境准备:一主两从,启动任一从机时,启动哨兵模式

虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵(sentinel)。

10-NoSQL_Redis_缓存_40


2,配置哨兵模式

1,配置哨兵

哨兵主要是用来监听主服务器的,所以一般把哨兵部署在从服务器上监听。

启动哨兵进程,首先需要创建哨兵配置文件vi sentinel.conf,可从源码配置redis-5.0.5/sentinel.conf中复制内容,也可以直接自定义该文件到bin目录下

在配置中输入:


sentinel monitor mastername 内网IP(127.0.0.1) 6379 1


10-NoSQL_Redis_集群_41


说明:

  • mastername:监控主数据的名称,自定义;
  • 127.0.0.1:监控主数据库的IP;
  • 6379:端口;
  • 1:最低通过票数;

2,启动哨兵

哨兵是一个单独的进程,启动之前确保主从服务是正常的。先启动主服务,后启动从服务

把日志写入指定的文件


[root@localhost bin]# ./redis-sentinel ./sentinel.conf >sent.log & 
[1] 3373


启动redis服务后,程序会自动配置文件sentinel.conf,并生成内容。注意:若再起启动需要删除下生成的内容。

10-NoSQL_Redis_开课吧_42


哨兵启动方式(注意redis.conf位置)


[root@localhost bin]# ./redis-server sentinel.conf --sentinel


10-NoSQL_Redis_开课吧_43


哨兵进程控制台:为master数据库添加了一个监控.

10-NoSQL_Redis_开课吧_44


同时多了哨兵进程:

10-NoSQL_Redis_开课吧_45


查询配置文件sentinel.conf中生成的内容: 

启动哨兵的时候,修改了哨兵的配置文件。如果需要再次启动哨兵,需要删除myid唯一标示。 

(保险的做法就是启动的一次,新配置一次)

10-NoSQL_Redis_缓存_46


3,主机宕机

机房意外:断电了。硬件故障:硬盘坏了。(这里用kill进程来模拟这种情况)

10-NoSQL_Redis_redis_47


哨兵控制台:从库自动提升为主库。同时也会自动修改redis.conf的主从配置文件。指向了新主机。再次启动原有的主机,原有的主机会变为从机。 

10-NoSQL_Redis_开课吧_48


10-NoSQL_Redis_redis_49


总结:

主从集群:主机有写入权限。从机没有,只有可读。

意外宕机方案:

  • 手动恢复:人为重启服务器,主机宕,把从机设置为主机。
  • 自动恢复:使用哨兵监控。自动切换主从。

十二,redis集群

1, redis-cluster架构

架构细节

  • (1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽;
  • (2)节点的fail是通过集群中超过半数的节点检测有效时,整个集群才生效;
  • (3)客户端与redis节点直连,不需要中间proxy层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可;
  • (4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value;

10-NoSQL_Redis_集群_50


Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽, redis 会根据节点数量大致均等的将哈希槽映射到不同的节点

10-NoSQL_Redis_开课吧_51


2,redis-cluster投票:容错

心跳机制

(1)集群中所有master参与投票,如果半数以上master节点与其中一个master节点通信超过(cluster-node-timeout),认为该master节点挂掉.;

(2)什么时候整个集群不可用(cluster_state:fail)?

  • 如果集群任意master挂掉,且当前master没有slave,则集群进入fail状态。也可以理解成集群的[0-16383]slot映射不完全时进入fail状态。
  • 如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。

3,redis集群搭建步骤

3.1 集群搭建

1,安装redis 

2,创建集群目录


[root@localhost redis]# mkdir redis-cluster


3,在集群目录下创建节点目录

10-NoSQL_Redis_缓存_52


搭建集群最少也得需要3台主机,如果每台主机再配置一台从机的话,则最少需要6台机器。 设计端口如下:创建6 个redis实例,需要端口号7001~7006


[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(cluster-enable 是否支持集群,默认是注释掉的)

10-NoSQL_Redis_集群_53


6,修改端口

10-NoSQL_Redis_集群_54

​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机器的端口 

9,启动7001-7006这六台机器,写一个启动脚本:自定义shel脚本


[root@localhost redis-cluster]# vi startall.sh


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 5.0.5中使用redis-cli --cluster替代redis- trib.rb,命令如下


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 --cluster-replicas 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:7002Adding replica 127.0.0.1:7006 to 127.0.0.1:7003
[OK] All 16384 slots covered.


10-NoSQL_Redis_开课吧_55


3.2 连接集群


[root@localhost 7001]# ./bin/redis-cli -h 127.0.0.1 -p 7001 -c


-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


关闭防火墙:service iptables stop

查看防火墙状态:service iptables status

3.3 查看集群信息


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


查看节点信息(cluster nodes)

10-NoSQL_Redis_缓存_56


4,Jedis连接redis集群

1,关闭防火墙 

注意:如果redis重启,需要将redis中生成的dump.rdb和nodes.conf文件删除,然后再重启。

2,代码实现


<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>


注意jedis的版本(这里用的是redis5.0.5),其他版本有可能报错:java.lang.NumberFormatException: For input string: "7002@17002"


public class Demo3 {
public static void main(String[] args) throws IOException {
//1.创建一个集合,保存集群信息
Set set=new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.197.132",7001));
set.add(new HostAndPort("192.168.197.132",7002));
set.add(new HostAndPort("192.168.197.132",7003));
set.add(new HostAndPort("192.168.197.132",7004));
set.add(new HostAndPort("192.168.197.132",7005));
set.add(new HostAndPort("192.168.197.132",7006));
//2.创建集群操作对象
JedisCluster jedisCluster = new JedisCluster(set);
//3.操作数据
//jedis中的方法和redis中的指令是相同的
jedisCluster.set("demo12","mydemo12");
String demo12 = jedisCluster.get("demo12");
System.out.println(demo12);
//4.关闭资源
jedisCluster.close();
}
}


10-NoSQL_Redis_集群_57


十三,缓存雪崩、缓存穿透和缓存击穿

更详细的解决方案可以参考这里​​@zeb_perfect【缓存穿透,缓存击穿,缓存雪崩解决方案分析】​​

1,缓存的概念

广义的缓存就是在第一次加载某些可能会复用数据的时候,在加载数据的同时,将数据放到一个指定的地点做保存。再下次加载的时候,从这个指定地点去取数据。这里加缓存是有一个前提的,就是从这个地方取数据,比从数据源取数据要快的多。

java狭义一些的缓存,主要是指三大类

  • 虚拟机缓存(ehcache,JBoss Cache)
  • 分布式缓存(redis,memcache)
  • 数据库缓存

正常来说,速度由上到下依次减慢

缓存取值图

10-NoSQL_Redis_分布式锁_58


2,缓存雪崩

缓存雪崩通俗简单的理解就是:由于原有缓存失效(或者数据未加载到缓存中),新缓存未到期间(缓存正常从 Redis中获取,如下图),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力, 严重的会造成数据库宕机,造成系统的崩溃。

10-NoSQL_Redis_分布式锁_59


缓存失效的时候如下图:

10-NoSQL_Redis_开课吧_60


解决方案:

方案1

在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据 和写缓存,其他线程等待。虽然能够在一定的程度上缓解了数据库的压力但是与此同时又降低了系统的吞吐量。


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;
}


注意:加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这时过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法。


方案2

分析用户的行为,不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

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;
}


注意:再给对应的ip存放真值的时候,需要先清除对应的之前的空缓存。

4,缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。

这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于,这里针对某一key缓存,前者则是很多key。

热点key:某个key访问非常频繁,当key失效的时候有大量线程来构建缓存,导致负载增加,系统崩溃。

解决办法:

  • 使用锁,单机用synchronized,lock等,分布式用分布式锁。
  • 缓存过期时间不设置,而是设置在key对应的value里。如果检测到存的时间超过过期时间则异步更新缓存。

十五,redis分布式锁

1,使用分布式锁要满足的几个条件

1. 系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现)

2. 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL)

3. 同步访问(即有很多个进程同时访问同一个共享资源。)

 

2,什么是分布式锁?

线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

3,应用的场景

线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。

有这样一个情境,线程A和线程B都共享某个变量X。

  • 如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。
  • 如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

分布式锁可以基于很多种方式实现,比如zookeeper、redis...。不管哪种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。

这里主要讲如何用redis实现分布式锁。

4,使用redis的setNX命令实现分布式锁

4.1 原理

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。

4.2 命令解析

1,setNX(SET if Not eXists,如果不存在则 SET)

将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。设置成功,返回 1 。设置失败,返回 0 。


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"


所以我们使用执行下面的命令SETNX可以用作加锁原语(locking primitive)。比如说,要对关键字(key) foo 加锁,客户端可以尝试以下方式:


SETNX lock.foo <current Unix time + lock timeout + 1>


  • 如果 SETNX返回 1 ,说明客户端已经获得了锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。 之后客户端可以通过 DEL lock.foo 来释放锁。
  • 如果 SETNX返回 0 ,说明 key 已经被其他客户端上锁了。如果锁是非阻塞(non blocking lock)的,我们可以选择返回调用,或者进入一个重试循环,直到成功获得锁或重试超时(timeout)。

2,GETSET

先获取key对应的value值。若不存在则返回nil,然后将旧的value更新为新的value。

将给定 key 的值设为 value ,并返回 key 的旧值(old value)。当 key 存在但不是字符串类型时,返回一个错误。


GETSET key value


10-NoSQL_Redis_开课吧_61


注意:

  • 1、同一时刻只能有一个进程获取到锁。setnx
  • 2、释放锁:锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;(最简单的方式就是del, 如果在删除之前死锁了。)

4.3 解决死锁

如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?

我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。

发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次(讲道理,删除锁的操作应该是锁拥有者执行的,这里只需要等它超时即可),当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:

  1. C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。
  2. C1 发送DEL lock.foo C1发送SETNX lock.foo 并且成功了。 C2 发送DEL lock.foo C2 发送SETNX lock.foo 并且成功了。 这样一来,C1,C2都拿到了锁!
  3. 幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0 C3发送GET lock.foo 以检查锁是否超时了。
  4. 如果没超时,则等待或重试。
  5. 如果已超时,C3通过下面的操作来尝试获得锁: GETSET lock.foo 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。

留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。

注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。

10-NoSQL_Redis_分布式锁_62


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;
}
}
}