Redis
一、NoSQL数据库概述
- NoSQL(NoSQL = Not Only SQL),意即“不仅仅是SQL”,泛指非关系型的数据库。
- NoSQL不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
- 不遵循SQL标准。
- 不支持ACID。
- 远超于SQL的性能。
NoSQL适用场景
- 对数据高并发的读写
- 海量数据的读写
- 对数据高可扩展性的
NoSQL不适用场景
- 需要事务支持
- 基于sql的结构化查询存储,处理复杂的关系,需要条件查询
什么时候用NoSQL?
用不着sql的时候和用了sql也不行的时候
二、Memcached和Redis的区别
Memcached:
- 数据都在内存中,一般不持久化
- 支持简单的key-value模式(String)
Redis:
- 几乎涵盖了Memcached的绝大部分功能
- 数据都在内存中,支持持久化,主要用作备份恢复
- 支持多种数据结构的存储:
- String(字符串)
- list(链表)
- set(集合)
- zset(有序集合)
- hash(哈希类型)
三、Redis安装
在Linux虚拟机中输入
$ wget http://download.redis.io/releases/redis-6.0.6.tar.gz
$ tar xzf redis-6.0.6.tar.gz
$ cd redis-6.0.6
$ make
$ make install
启动redis
$ cd /opt/
$ redis-server
我们可以让redis在后台进行启动
备份redis.conf文件
$ mkdir /opt/myRedis
$ cp redis.conf /opt/myRedis/redis.conf
$ cd /opt/myRedis
$ vim redis.conf
输入“/”查找daemonize,将no改为yes,输入“:wq”保存
输入命令即可启动redis,如果是在别的目录需要加上文件路径
$ redis-server redis.conf
开启客户端,也可以指定ip和端口号
$ redis-cli
$ redis-cli -h 127.0.0.1 -p 6379
输入ping,如果返回pong,则说明连接成功
如果要关闭redis,输入指令
$ redis-cli shutdown
四、Redis介绍
Redis是单线程+多路IO复用技术
多路复用是指使用一个线程来检查多个文件描述符(Socket) 的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真
正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
串行vs多线程+锁(memcached) vs 单线程+多路IO复用(Redis)
原子性
所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch (切换到另一 个线程)。
1)在单线程中, 能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间。
2)在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
Redis单命令的原子性主要得益于Redis的单线程
java中的i++不是原子操作,原因是:i++的操作步骤为取出、自增、存入;这三个阶段中间都可以被中断分离开的,所以i++不是原子操作
1、基本指令
keys *:查看当前库的所有键
exists :判断某个键是否存在
type :查看键的类型
del :删除某个键
expire :为键值设置过期时间,单位秒
ttl :查看还有多少秒过期,-1表示永不过期,-2表示已过期
dbsize:查看当前数据库的key
flushdb:清空当前库
flushall:通杀全部库
2、数据类型
1、String
指令:
set :添加键值对
get :查询对应键值对
append :添加新的value到指定key的末尾
strlen :获得值的长度
setnx :只有在key不存在的时候,才设置键值对
setex <过期时间> :设置键值的同时,设置过期时间,单位秒
mset …:同时设置一个或多个键值对
mget …:同时获取一个或多个value
msetnx …:当key不存在的时候,设置一个或多个键值对
incr :将key中储存的数字值加1,只能对纯数字值操作。如果为空,新增值为1
decr :值减1,只能对纯数字值操作。如果为空,新增值为-1
incrby/decrby <步长>:将key中存储的数字值增减。可以自定义增减的值
getrange <起始位置> <结束位置>:获得值的范围,类似java中的substring
setrange <起始位置> :用覆盖所储存的字符串值,从<起始位置>开始
getset :设置新的值同时获得旧值
2、List
- 单键多值
- Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一一个元
素导列表的头部(左边)或者尾部(右边)。 - 它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操
作中间的节点性能会较差。
指令:
lpush/rpush …:从左边/右边插入一个或多个值
lpop/rpop :从左边/右边吐出一个值,如果键中的值为空,键消失
rpoplpush :从列表右边吐出一个值,插到列表左边
lrange :按照索引下标获得元素(从左到右,如果从头查到尾,就是0到-1)
lindex :按照索引下标获得元素(从左到右)
llen :获得列表长度
linsert before/after :在value的前面/后面插入newvalue值
lrem :从左边删除n个value(从左到右,如果n为负数代表从右往左,0代表删除所有)
3、set
- Redis的set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
- Redis的set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。
指令:
sadd :将一个或多个值添加到集合key中,已经存在于集合的值将忽略
smembers :取出该集合的所有值
sismember :判断集合是否包含值,有返回1,否则返回0
scard :返回该集合的元素个数
srem …:删除集合中的某个元素
spop :随机从该集合中吐出一个值
srandmember :随机从该集合中取出n个值,不会从集合中删除
sinter :返回两个集合的交集元素
sunion :返回两个集合的并集元素
sdiff :返回两个集合的差集元素
4、hash
- Redis hash是一个键值对集合。
- Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
- 类似Java里面的Map <String, String>
指令:
hset :给集合中的键赋值,这里key是redis的key,field是hash中的key
hget :从集合中取出值
hmset …:批量设置hash的值
hexists :查看哈希表key中,给定域field是否存在
hkeys :列出该hash集合的所有field
hvals :列出该hash集合的所有value
hincrby :为哈希表key中的域field的值加上增量increment
hsetnx :将哈希表key中的域field的值设置为value,当且仅当域field不存在
hgetall:获取所有键值对
5、zset
- Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了。
- 因为元素是有序的,所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
指令:
zadd …:将一个或多个元素及score值加入到有序集key中
zrange 之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集
zrangebyscore/zrevrangebyscore [WITHSCORES] [LIMIT offset count]:返回有序集key中,所有score值介于min和max之间(包含)的成员。有序集成员按score值递增(从小到大)/(从大到小)次序排列
zincrby :为元素的score加上增量
zrem :删除该集合下制定值的元素
zcount :统计该集合,分数区间内的元素个数
zrank :返回该值在集合中的排名,从0开始
五、Java连接Redis
- ip地址的绑定(bind)
默认情况bind=127.0.0.1只能接受本机的访问请求。不写的情况下,无限制接受任何ip地址的访问。生产环境肯定要写你应用服务器的地址,如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的相应。
使用vim命令编辑redis的配置文件
$ vim /opt/myRedis/redis.conf
将图片中的这句话注释掉
将protected-mode yes改为no
启动redis
$ redis-server /opt/myRedis/redis.conf
新建java项目,我这里使用maven创建项目
引入jedis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
创建一个类进行简单测试
public class RedisDemo {
public static void main(String[] args) {
// 填写虚拟机的ip地址以及端口号
Jedis jedis = new Jedis("192.168.0.113", 6379);
String res = jedis.ping();
System.out.println(res);
// 关闭jedis
jedis.close();
}
}
控制台打印PONG说明连接成功
六、Redis事务
- Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- Redis事务的主要作用就是串联多个命令防止别的命令插队
- Multi、Exec、Discard(开启事务、执行事务、取消事务)
- 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
- 组队的过程中可以通过discard来放弃组队。
- 事务的错误处理:
- 组队中某个命令出现了错误,执行时整个队列都会被取消
- 执行阶段某个命令出现错误,则只有报错的命令不会被执行,而其他命令都会执行,不会回滚
- WATCH key [key…]:在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个)key,如果在事务执行之前这个(或这些) key被其他命令所改动,那么事务将被打断。
- UNWATCH:取消WATCH命令对所有key的监视,如果在执行WATCH命令之后,EXEC命令或DISCARD命令先被执行了的话,那么就不需要再执行UNWATCH了。
使用WATCH进行监视
新建一个窗口,修改a的值为123
之后回到原来的窗口进行执行,提示nil,说明这个事务被取消了
- 事务三特性:
- 单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 - 没有隔离级别的概念
队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题 - 不保证原子性
Redis同一个事务中如果有一 条命令执行失败,其后的命令仍然会被执行,没有回滚
- 悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
- 乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check- and-set机制实现事务的。
七、Redis持久化
Redis提供了2个不同形式的持久化方式
- RDB(Redis DataBase)
- AOF(Append Of File)
1、RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
1、RDB保存的文件
RDB保存的文件名,是在redis.conf中配置文件的名称,默认为dump.rdb,可修改
而RDB文件保存的路径也可修改,默认为Redis启动时命令行所在的目录下,也就是在哪个目录开启的redis,这个RDB文件就保存在哪
我们可以修改路径,将它保存在我们之前创建的文件夹/opt/myRedis里
2、RDB文件保存的策略
save :在秒内对数据库进行次修改,就会进行保存
比如save 300 10,就是在300秒内,对redis进行10次修改,就进行一次保存。只要满足其中一个保存策略,就会进行保存。
3、备份是如何执行的
Redis会单独创建(fork函数)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,父进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感(允许丢失一些数据),那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
而这个fork函数,就是使用的系统多进程COW(Copy On Write)机制,这个机制会产生一个和父进程完全相同的子进程,而主进程和子进程会共享内存里面的代码块和数据段,快照持久化就可以完全交给子进程来做,父进程继续处理客户端请求。而子进程一旦创建,进程中的数据就不会改变,这也是为什么叫做快照的原因。
为了方便测试,这里将保存策略save 300 10改为了30 5,在没有添加数据之前,dump.rdb大小为92字节,我们添加五条数据,文件大小变成102了。
除了save的保存策略以外,使用shutdown正常关闭redis也会进行备份
2、AOF
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
AOF默认不开启,需要将配置文件中的appendonly no改为yes才能开启
1、AOF保存的文件
AOF保存的文件名,是在redis.conf中配置文件的名称,默认为appendonly.aof,可修改
保存的路径与RDB文件保存的路径相同
2、AOF同步频率设置
AOF有以下三种同步频率设置
- 始终同步,每次Redis的写入都会立刻记入日志
- 每秒同步,每秒记入一次日志,如果宕机,本秒的数据可能丢失
- 不主动进行同步,把同步时机交给操作系统
默认是每秒记录一次
3、备份是如何进行的
AOF的备份机制和性能虽然和RDB不同,但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis。工作目录下,启动系统即加载。
AOF重写:
AOF采用文件追加方式,文件会越来越大。为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。比如set a a,set a b,会保留set a b这条指令。也可以使用命令bgrewriteaof进行重写。
AOF文件持续增长而过大时,会fork出一 条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,如果Redis的AOF当前大小>=base_size+base_size*100%(默认,也就是大于等于原来的2倍)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
我们向redis中添加一条数据
可以看到,添加一条数据之后,appendonly.aof文件大小从0变为50,而执行读操作则不变
如果AOF和RDB同时开启,redis默认优先使用AOF进行持久化操作
3、两者的区别
1、RDB
存的是数据
RDB的优点:
- 节省磁盘空间
- 恢复速度快
RDB的缺点:
- 虽然Redis在fork时使用了写时拷贝技术(COW),但是如果数据庞大时还是比较消耗性能。
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
2、AOF
存的是指令
AOF的优点:
- 备份机制更健全,丢失数据概率低
- 可读的日志文本,通过操作AOF文件,可以处理误操作
AOF的缺点:
- 比起RDB占用贡多磁盘空间
- 恢复备份速度要慢
- 每次读写都同比的话,有一定的性能压力
- 存在个别bug,造成无法恢复
八、Redis主从复制
1、什么是主从复制
主从复制就是主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主。主服务器只能有一个,从服务器可以有多个。
用处:
- 读写分离,减少主服务器和从服务器的读写压力,性能扩展
- 容灾快速恢复
配从(服务器)不配主(服务器)
- 拷贝多个redis.conf文件include
- 开启daemonize yes
- Pid文件名字pidfile
- 指定端口port
- Log文件名字
- Dump.rdb名字dbfilename
- 关掉Appendonly或者改名
2、如何建立主从关系
我们直接来做个简单的例子,创建三个redis服务器,一个作为主服务器,两个作为从服务器
我们还是在/opt/myRedis下新建一个redis配置文件,作为主服务器的配置文件
$ vim redis6379.conf
在文件中输入以下命令
include /opt/myRedis/redis.conf
pidfile /var/run/redis6379.pid
port 6379
dbfilename dump6379.rdb
我们还要将redis.conf中的appendonly改为no
之后我们复制两份redis6379.conf文件,作为两个从服务器的配置文件,分别命名为redis6380和redis6381
$ cp redis6379.conf redis6380.conf
$ cp redis6379.conf redis6381.conf
之后我们需要修改两个从服务器的配置文件
$ vim redis6380.conf
这里使用:%s可以替换文本,将6379改为6380,另一个也是一样的操作
之后可以启动redis服务了
$ redis-server redis6379.conf
$ redis-server redis6380.conf
$ redis-server redis6381.conf
可以输入指令查看服务是否启动
$ ps -ef | grep redis
可以看到当前connected_slaves为0
我们让6379端口作为主服务器,另外两个作为从服务器。输入以下命令可以指定一个客户端作为该客户端的主服务器
slaveof <ip> <port>
我们在6380和6381端口下指定6379作为主服务器
slaveof 127.0.0.1 6379
再次查看6379端口的主从关系
这里已经显示两个从服务器的信息了,主从关系已经建立。
输入以下代码可以去除从服务器的身份
slaveof no one
但是这里建立的主从关系只是临时的,当服务器全部关闭再打开时,并不会保留之前的主从关系。想要让主从关系永久保留,我们可以修改配置文件,输入主服务器ip和端口,这样每次开启就能自动建立主从关系。
slaveof <masterip> <masterport>
注意:
- 主从数据是同步的
- 主服务器只能进行写操作,从服务器只能进行读操作
- 从服务器掉线之后,需要输入命令slaveof来重新指定主服务器,重连之后会把缺少的数据补上。
- 主服务器掉线之后,从服务器会进入待机状态,直到主服务器恢复
3、主从之间数据如何同步
——以下转自《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU
启动一台slave的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave
简单来讲就是:
- 每次从机联通后,都会给主机发送sync指令
- 主机收到命令后,立刻进行存盘操作(RDB),发送RDB文件给从机
- 从机收到RDB文件后,进行加载
- 之后每次主机的写操作,都会立刻发送给从机,从机执行相同的命令
4、哨兵(sentinel)
哨兵模式能够后台监视主机是否故障,如果故障了根据投票数自动将从服务器变为主服务器。
想要开启哨兵模式,需要在目录下创建sentinel.conf配置文件,并且在配置文件中填写内容:
sentinel monitor mymaster 127.0.0.1 6379 1
其中,mymaster是为监控对象起的自定义服务器名称,1为至少有多少个哨兵统一迁移的数量
之后启动哨兵,输入命令
redis-sentinel sentinel.conf
我们还是让6380和6381作为6379的从服务器
此时我们将6379主服务器shutdown,可以在打印的日志中看到,已经将6380变为主服务器了,并且在下面代码中,显示已经将6379变为6380的从服务器。6379服务器上线之后,哨兵会自动将6379变为从服务器。
在6380端口查看主从关系,role变为了master
哨兵模式的特点:
- 从下线的主服务器的所有从服务器里挑选一个从服务器,将其变为主服务器,选择条件依次为:
- 选择优先级靠前的
- 选择偏移量大的
- 选择runid最小的
其中,优先级在redis.conf中的默认值为slave-priority 100;偏移量是指获得原主数据最多的;每个redis示例启动后都会随机生成一个40位的runid
- 挑选出新的主服务之后,sentinel向原主服务的从服务发送slaveof新主服务的命令,复制新master
- 当已下线的服务重新上线时,sentinel会向其发送slaveof命令,让其成为新主的从服务器
九、缓存雪崩、击穿、穿透
1、雪崩
如果缓存数据设置的过期时间是相同的,并且Redis恰好将这部分数据全部删光了。这就会导致在这段时间内,这些缓存同时失效,全部请求到数据库中
- Redis挂掉了,请求全部走数据库
- 对缓存数据设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库
缓存雪崩如果发生了,很可能就把我们的数据库搞垮,导致整个服务瘫痪
解决方法:
1、对于“对缓存数据设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库。”这种情况,非常好解决
- 在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。
setRedis (Key,value,time + Math.random() * 10000)
2、对于“Redis挂掉了,请求全部走数据库”这种情况,我们可以有以下的思路:
- 事发前:Redis高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
- 事发中:本地ehcache缓存+Hystrix限流+降级,避免MySQL被打死。
- 事发后:Redis持久化RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
2、穿透与击穿
1、缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。
像这种你如果不对参数做校验,数据库id都是大于0的,我一直用小于0的参数去请求你,每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。
解决方法:
1、可以在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。
2、从缓存取不到的数据,在数据库中也没有取到,这时也可以将对应Key的Value对写为null、位置错误、稍后重试等,看具体的场景,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。
3、布隆过滤器(Bloom Filter),这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。
2、缓存击穿
跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
解决方法:
缓存击穿的话,设置热点数据永远不过期。或者加上互斥锁就能搞定了