redis学习从练气到化虚

redis

  • 一、NoSQL
    • 1.什么是NoSql
    • 2.NoSql的特点
    • 3.非关系型数据库有哪些
  • 二、认识Redis
    • 1.Redis是什么
    • 2.Redis能干嘛
    • 3.Redis的特性
    • 4.redis的一些基本命令
    • 5.redis是单线程为什么还这么快
    • 6.redis-benchmark 压测工具
  • 三、五大基本数据类型和三种特殊数据类型
    • 1.String字符串类型
    • 2.List列表类型
    • 3.Set集合类型
    • 4.Hash哈希类型
    • 5.ZSet(Sorted Set)有序集合类型
    • 6.Geospatial地址位置 /ˌdʒiːəʊˈspeɪʃəl/
    • 7.Hyperloglog基数统计
    • 8.Bitmap位图
  • 四、redis的事务与其他常见事务场景
    • 1.Redis的事务
    • 2. Spring的事务支持
    • 3.Mybatis的事务支持
    • 4.Mysql的事务支持
    • 5.分布式架构中的事务
    • 6.redis整合jedis和springboot
  • 五、reids面试高频点
    • 1.Redis实现乐观锁
    • 2.Redis配置文件详解
    • 3.持久化RDB操作
    • 4.持久化AOF操作
    • 5.Redis订阅发布
    • 6.Redis的主从复制
    • 7.哨兵模式详解(sentinel)
    • 8.分布式锁
    • 9.缓存穿透
    • 10.缓存击穿
    • 11.缓存雪崩

一、NoSQL

1.什么是NoSql

NoSQL最常见的解释是“non-relational”, “Not Only SQL”也被很多人接受。NoSQL仅仅是一个概念,泛指非关系型的数据库,区别于关系数据库,它们不保证关系数据的ACID特性。NoSQL是一项全新的数据库革命性运动,其拥护者们提倡运用非关系型的数据存储,相对于铺天盖地的关系型数据库运用,这一概念无疑是一种全新的思维的注入。
NoSQL有如下优点:易扩展,NoSQL数据库种类繁多,但是一个共同的特点都是去掉关系数据库的关系型特性。数据之间无关系,这样就非常容易扩展。无形之间也在架构的层面上带来了可扩展的能力。大数据量,高性能,NoSQL数据库都具有非常高的读写性能,尤其在大数据量下,同样表现优秀。这得益于它的无关系性,数据库的结构简单。

2.NoSql的特点

①易于扩展,因为数据之间没有关联关系几乎没有耦合,扩展起来会很方便。
②大数据量高性能(比如redis,每秒可以读11万次,写8万次)
③数据类型多样性,不用预先建立表结构(无需设计数据库)
以上便是NoSql的典型特征,相比之下关系型数据库的特征也很明显了,严格的表结构,sql查询、严格的事务机制等。

3.非关系型数据库有哪些

非关系型数据库主要有四种类型:键值对数据库(Redis)、列存储数据库(HBase)、文档型数据库(MongoDb)、图形数据库(Neo4J),每种NoSql都有各自的特点。
redis学习从练气到化虚_第1张图片

二、认识Redis

1.Redis是什么

Redis全称是Remote Dictionary Server远程字典服务,她是一个基于C语言编写实现,可基于内存亦可持久化的日志型key-value数据库,支持多重语言的API,比如Java、Pythen、PHP等,免费开源,是当下最热门的NoSql数据库之一命令不区分大小写。学习redis首选redis中文官方网站:http://www.redis.cn/

2.Redis能干嘛

①内存存储,持久化数据,因为内存是断电即失所以持久化对于Redis来说是很重要的,Redis的持久化RDB、AOF。
②存取效率高,可用于高速缓存
③发布订阅系统,可充当消息队列
④地图信息分析
⑤计时器、计数器等

3.Redis的特性

①多样的数据类型
②支持持久化
③支持主从、集群
④支持事务

4.redis的一些基本命令

指定配置文件启动redis
./redis-server /usr/redis-6.0.1/redis.conf

连接客户端:
./redis-cli -p 6379

测试连接是否成功
ping

查看reddis进程
ps -ef|grep redis

客户端中停止redis
shutwon

切换redis库(0-16)
select 00代表0号库,切换几号库就输入几号)

查看当前库中所有key的数量
dbsize

查看所有的key
keys *

清空当前所处的redis库
flushdb

清空所有redis库
flushall

以上是一些系统级别的命令,下面列举一些通用的key-value的命令

判断keyName这个键是否存在
exists keyName

将keyName从当前数据库移走,1代表当前数据库(一般不使用该命令移除)
move keyName 1

设置keyName 10s过期,默认单位就是秒
expire keyName 10

查看keyName的过期时间:-2已经过期,-1永久有效,其余代表该键生命所剩秒数
ttl keyName

查看keyName的数据类型
type keyName

5.redis是单线程为什么还这么快

redis是基于内存操作的,cpu并不是redis的性能瓶颈,影响redis性能的主要因素是内存的大小和网络带宽,那为什么基于内存的单线程可以很快呢,因为在单核cpu中多线程的实现是通过cpu的调度(切换时间片)来实现的,而切换时间片也是需要时间的,若是直接使用单线程就会少了这次切换,而且对于内存系统来说,对于没有上下文切换效率就是最高的,切换上下文无疑会有延时。

6.redis-benchmark 压测工具

redis-benchmark是redis自带的一种测试工具,redis号称读每秒11万,写每秒8万次就是根据这个工具测算而来,我们也可以自己测试一下是否是如官方所说真的支持这么高的性能。下面先来看下这个工具所支持的一些命令参数:

  • redis-benchmark的命令参数

    参数 描述 不传该参数的默认值
    -h 指定服务器主机名 127.0.0.1
    -p 指定redis端口号 6379
    -s 指定服务器Socket
    -c 指定并发连接数 50
    -n 指定请求数 10000
    -d 以字节的形式指定get/set 值的数据大小 2
    -k 1代表keep alive 0代表reconnect 1
    -r set/get/incr使用随机key,SADD使用随机值
    -p 通过管道传输请求
    -q 强制退出redis,仅显示query/sec 值
    -csv 以csv形式输出
    -t 运行以逗号分割的测试命令列表
  • 测试100个请求,每个请求10000次的并发场景,测试命令:./redis-benchmark -h localhost -p 6379 -c 100 -n 10000,测试结果如下图(部分测试结果):

    [root@localhost src]# ./redis-benchmark -h localhost -p 6379 -c 100 -n 10000
    ====== PING_INLINE ======
      10000 requests completed in 0.11 seconds   #表示创建10000个请求用了0.11100 parallel clients                       #表示100个客户端并发执行
      3 bytes payload                            #表示每个值大小是3个字节
      keep alive: 1                              #表示保持连接
    
    87.30% <= 1 milliseconds
    97.83% <= 2 milliseconds
    98.01% <= 5 milliseconds
    98.29% <= 6 milliseconds
    98.93% <= 7 milliseconds
    99.32% <= 8 milliseconds
    99.95% <= 9 milliseconds
    100.00% <= 9 milliseconds
    88495.58 requests per second                  #表示每秒处理了 88495个请求
    
    ====== PING_BULK ======
      10000 requests completed in 0.10 seconds
      100 parallel clients
      3 bytes payload
      keep alive: 1
    
    84.34% <= 1 milliseconds
    99.06% <= 2 milliseconds
    100.00% <= 2 milliseconds
    99009.90 requests per second
    

    从上面的测试结果来看redis的性能还是很高的,redis-benchmark不仅仅会创建这些请求,还会去执行set、get、incr、lpush、lpop等等这些请求来测试redis的性能,反正一通测试后我们可以发现redis很强大,是一种作为缓存、数据库、消息队列的良好选择。

三、五大基本数据类型和三种特殊数据类型

这一节用来总结Redis支持的所有数据类型,总共有8种数据类型,其中5种基本的数据类型也是比较常用的,此外还有三种涉及到地理位置、基数统计、位图的数据类型,如下。

1.String字符串类型

据说绝大部分程序员都只使用redis的String类型,一方面可以说明很多人对Redis的数据类型使用有线了解也有限,还可以说明String类型的强大和重要性。
String类型的一些基本命令:

查看String类型的所有命令
help @String

设置String类型的值:
set key value

获取string类型的值:
get key

设置带过期时间的String(表示这组键值对10秒过期)
setex key 10 value

设置键值对时先查询是否存在,不存在再设置(set not exist)
setnx key value

同时设置多个值,键值依次空格隔开即可(这个是有些意思的)
mset k1 v1 k2 v2 k3 v3

同时获取多个值,键依次空格隔开即可(原子操作)
mget k1 k2 k3

不存在设置的批量操作(其中有一个存在,整个都会失败,因为是原子性的)
msetnx k1 v1 k2 k2 v2 k3 v3

先获取再设置,若是不存在则直接设置返回nil,存在的话会覆盖返回返回原来的值
getset k1 v1

在key的值后面追加一个hello字符串(不存在该key,就会创建)
append key hello

获取key的值的长度
strlen key

将key对应的值做++处理(值是String时,值必须是合法的数字,非数字报错)
incr key

将key对饮的值做--处理(值是String时,值必须是合法的数字,非数字报错)
decr key

将key对应的值加10处理
incrby key 10

将key对应的值减10处理
decrby key 10

获取key对应字符串的0-3下标的子字符串,3是要截取的尾部下标,包含3,若是截取全部,尾部参数传-1
getrange key 0 3

替换key对应字符串中从1开始的字符串(rep是新值,他有多长久替换多长的原始字符串)
setrange key 1 rep


2.List列表类型

list数据类型非常类似于java中的list集合,他支持任何数据类型的加入(不过事实上都是存储的list,五种基本数据类型里面存储的数据都是string),且数据元素支持重复,且大部分的list的命令都是以L开头的,但是这里的L并不是严格的表示list,笔者感觉可能还代表Left的意思,比如我们使用lpush将值存入list时,所有的值都是存在最左面的,同样我们依然可以使用rpush 来将值存在最右面,下面来大致看下list的操作命令

将one存入到list1的最左面(可以理解为链表的头插法)
lpush list1 one

移除list1的最左面的元素
lpop list1

移除list1的最右面的元素
rpop list1

查看list1中下标从01的数据
lrange list1 0 1 

查看list1中全部的数据
lrange list1 0 -1

获取list1从左面开始下标为0的元素
lindex list1 0

查看list1 的长度
llen

移除list1中从左面开始前两个one(数字传0 可以全部移除,传其他数字都是代表移除的个数)
lrem list1 2 one

截取:截取list1中的一段保留,其他舍弃,键名称不变,如截取list1集合的第一个元素
ltrim list1 0 0

转移:将一个list中的最后一个元素移动到list1的第一个元素,list消失一个,list1多一个(list1不存在就新建)
rpoplpush list list1

根据下标插入:将list中的第一个元素替换为rep(list不存在,则不成功,若是下标越界也不会成功)
lset list 0 rep

在指定元素的前后插入:在list中的one前面插入一个first
linsert list before one first

在指定元素的前后插入:在list中的seven后面插入一个second
linsert list after seven second

list的操作命令支持的有很多,而且几乎所有的list命令都支持左右之分,比如lpush、rpush、lpop、rpop等等,试想一下若是我们使用lpush、rpop那么list就像是一个左进右出的队列,我们完全可以用来做消息处理,若是使用lpush、lpop命令list就相当于一个左进左出的栈,所以说list的功能还是很强大的,主要还是在于使用的人,引用某位大神的话来说牛逼的人用记事本一样可以做数据库,api调用工程师就只会get、set。

3.Set集合类型

说完了String、list然后比较常用的就是set了,redis中的set也和java比较像,他不支持重复、无序,java里面是因为set底层是map,map底层又使用了hash表所以不支持重复。这里不清楚是否是一个原理,但是数据结构都是相同的,集合在任何语言里都有,而且实现的数据结构也类似,所以说只要精通了其中一种,其他就不是什么难事了。下面来说下set的命令,string的命令没什么标识,list的命令很多都带有l,当然也有一部分用来支持从list的右边操作带有r的,set集合支持的命令一般都带有s。

添加元素:往myset中添加一个元素one
sadd myset one

查看所有元素:查看myset中的所有元素
smembers myset

查看元素是否存在:查看myset中是否含有four元素(0不存在,1存在)
sismember myset four

查看set集合元素个数:查看myset中元素的个数
scard myset

移除元素:移除myset中的three
srem myset threee

随机获取元素(抽奖):随机获取myset中的任一元素(1就是随机抽取一个,2随机抽取两个,抽完以后set集合不会变化)
srandmember myset 1

随机删除元素(抽奖): 随机删除myset中的一个元素(返回结果就是被删除的元素,1代表删除一个)
spop mhyset 1 

移动元素:将myset中的one移动到myset2中
smove myset myset1 one

集合操作取交集(共同好友):获取myset与myset1的交集元素
sinter myset myset1

集合操作取并集:获取myset与myset1的并集元素
sunion myset myset1

集合操作取差集:获取myset中myset1不存在的元素
sdiff myset myset1

set中比较有用的就是我们可以直接使用set做集合操作,比如获取两位博主的共同粉丝,获取共同好友然后做好友推荐(这个还挺有用)。

4.Hash哈希类型

这里介绍的顺序是根据使用的频率来进行介绍的,string、list、set、hash,这里就说到了hash,这里的hash也可以类比于java中的map,hash就是一个键值对,本来redis的数据存储就是键值对,而值又是键值对,所以他的结构就是这样>。相当于是map中存的又是map的感觉,这里的hash也是无续,不支持重复。前面看过来可以发现string的操作命令自成一套,而list的命令基本都是以l开头,lpush、lpop等,set命令都是以s开头sadd、smembers、sismember等,hash的命令呢,就有些和string类似了,他支持的命令都是以h开头的,但是命令主体与string比较类似,如hset、hget、hmset等,其实hash的本质还是string,只是一次存了两个string类型而已,因为你也并没有办法往hash中的值中存入除了string的其他类型。下面来一一列举下hash所支持的命令:

存储:往myhash中存入一组键值对key1,value1(myhash相当于外围key,key1才是hash的key,hash的存储结构,可以直接使用string的user:name:id这种格式实现)
hset myhash key1 value1

不存在再存储:若是myhash中不存在key5在存储key5键值对
hsetnx myhash key5 value5

获取:从myhash中获取key1对应的值
hget myhash key1

获取所有值:查看myhash中所有的键值对(展示时key和value是分开展示的key在上,value在下,然后下一组接着,就像是展示list一样)
hgetall myhash

同时设置多个键值对:往myhash中同时设置多个键值对
hmset myhash key1 value1 key2 value2

同时获取多个值:获取myhash中的key1、key2的值
hmget myhash key1 key2

删除一个值:从myhash中删除key1对应的值
hdel myhash key1

获取hash集合的长度:获取myhash的集合长度或者说叫元素个数
hlen myhash

判断键值对是否存在:判断myhash中是否存在key4键值对
hexists myhash key4

获取所有的key:获取myhash所有的key值
hkeys myhash

获取所有的value:获取myhash所有的value值
hvals myhash

自增:将myhash中key1的值进行自增1(自增多少可以自由设置,很奇怪,hash没有提供自减的方法,想要自减传负数即可)
hincrby myhash key1  1

hash的命令大致就这些,大部分都是会用到的,仔细看了这些命令的肯定会发现,这些命令与string类型别无二致,无非就是多了h这个前缀。前面说string时说过使用string时可以这么使用key=user:id:name value=张三,这么去存储,其实使用map则更好使一些,我们就可以直接将一批数据存储到hash中,而不用像string那样去创建很多个key,使用hash一个外围key就行,其他的都在hash的内部进行创建小key。

5.ZSet(Sorted Set)有序集合类型

ZSet是五种基本数据类型中的最后一种,他也是一个集合,不过他和set的区别是ZSet是有序的,而set是无序的。他俩还是比较相似的,因此在操作命令上很多也是类似。比如set的常规操作sadd、smembers、sismember等,下面列举下ZSet的命令:

插入一个元素:往myzset中插入一个元素one(这里的1不是指位置而是一个必须制定的数据,让其余数据域one进行绑定,排序时会用到,这个1通常被称为score或者number)
zadd myzset 1 one

一次插入多个元素:在myzset插入two,插入three,插入four
zadd myzset 2 two 3 three 4 four

查询所有的元素:查询myzset中所有的元素
zrange myzset 0 -1

将所有元素进行正序输出:将myzset中所有的元素进行正序排序然后输出(从小到大),这里的负无穷到正无穷他们任意一个都可以使用数字代替,但是他们的顺序不能变,意思就是只能从小到大,此外我们后面可以加参数withscore,这样就会连带score一起展示,还可以加limit,limit后面第一个参数表示从1下标开始,截取2个元素展示,
zrangebyscore myzset -inf +inf withscores limit 1 2		

将元素进行倒序输出:将myzset中的所有元素进行倒序输出,注意这里使用时,数字的大小不能超过真实值的范围,0 -1代表全部倒叙,其他数值是根据数值返回进行输出
zreverange myzset 0 -1 


移除一个元素:将myzset中的four元素移除
zrem myzset four	

获取集合中元素的个数:获取myzset中所有元素的个数
zcard myzset

根据score范围获取元素个数:统计myzset中score为13的元素个数
zcount myzset 1 3

排行榜可以使用zset来做,将数据的阅读量或者播放量存入score,然后根据score就可以自动排序,只需要每隔一段时间调用redis的zset集合就可以拿到最新的排行信息,当然这里是需要使用zrangbyscore指令的。
只要涉及到有序的场景其实都可以尝试使用zset集合。到这里基本数据类型就介绍完了,我们来看下每种基本数据类型最大值的支持。

类型 最大值
string 512MB
list 232-1
hash 232-1
set 232-1
zset 232-1

6.Geospatial地址位置 /ˌdʒiːəʊˈspeɪʃəl/

这个单词是个组合词,应该是geography spatial组成的,意思是地址空间的意思。
geospatial,可以实现的功能也很强大,比如实现定位,实现附近的人,实现打车距离的计算,位置共享等,这些应用场景在微信里应该都很好用,而这些都可以通过geospatial来实现,那geospatial是怎么实现这些场景的呢,是通过哪些命令来实现呢,其实geospatial一共也就只有6个命令,很好学,而且他的所有命令都是geo开头的,值得说的是geoadd这个存储命令其实是将信息存储到zset中的,所以存完以后我们使用zset也是可以看到他的信息的,这也是geospatial没有提供遍历命令的原因,下面来一起看下她的六个命令:

添加元素:往mygeo中添加一个经度为116.397128,纬度为39.916527,名称为‘beijing’的数据元素 city,也支持一次添加多个(实际就是往city这个zset中添加了北京这个元素,这忠地址数据一般使用java程序直接导入,不会去手动一个个录入的)
geoadd city 116.397128 39.916527 beijing 

根据远程名称查询经纬度:从city这个geo中查询tianjin、beijing的经度和纬度(pos是position位置的缩写)
geopos city tianjin beijing

查看两个地点间的距离:查看city这个geo中beijing和tianjin的距离,不过这里需要指定单位,单位如下,不指定单位时默认使用m,dist是distance距离的缩写
m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
geodist city beijing tianjin km

以给定的经度纬度为中心找出半径内的所有元素:查找city这个geo中经度为100纬度为50的位置半径是1000km以内的城市,同是后面还可以跟withdist展示每个位置距离中心的位置,withcoord展示元素的经度纬度,count展示数据的数量(相当于limit 从0开始截取),asc/desc正序倒叙排序
georadius city 100 50 1000 km

以给定的元素为中心找出半径内的所有元素:查询city这个geo中以tianjin为中心1000km以内的所有城市并按升序排序,其他参数的支持与georadius没什么区别。
georadiusbymember city tianjin 1000 km asc

获取一个位置的hash字符串,返回是一个11位(位数固定)的字符串,字符串比较相似则两个位置距离较近,这个命令基本没啥用,没人会去使用hash来判读距离,都是使用实际的geodist georadius等来判断(未来说不好用不到):获取tianjin和beijing的hash值
geohash city beijing tianjin
(geohash降维打击?哈哈,将经度纬度转换为一个11位的字符串)

查询所有的地理位置信息,因为geo这个信息都是使用zset存储的,我们直接使用zset的命令查看就行:查看city这个geo中所有的地点
zrange city 0 -1

7.Hyperloglog基数统计

这个单词也是一个组合词,Hyper这个单词很常见,比如http中的h就是hyper,为啥后面跟了俩log不清楚了不过这个不重要,重要的是hyperloglog的命令和他的作用,其实hyperloglog的作用也很大,就是进行统计计数,几乎所有的统计计数我们都可以使用hyperloglog来完成,比如我们统计网站的登录人数,当前视频的观看人数等都可以使用hyperloglog,不过必须要说的是hyperloglog的计数不是完全准确的当数据量很大时,可能会出现些许的误差,但误差不会太大,不过一般计数场景对数据的强一致性要求都不是太高,些许误差是允许的,若是要求不允许有误差就可以使用zset了,不过效率上肯定不及hyperloglog,而且hyperloglog占用内存空间相对于set、zset都小的很多,所以她是做基数统计的首选。此外hyperloglog只有三个命令很好学,我们一起看下,她的所有命令都是以pf开头的(以pf开头估计是因为h已经被hash用了)。

添加一组元素:往mylog中添加一组数据元素:1 2 3 a b c ti(hyperloglog类似于集合set,元素时一组一组的存放)
pfadd mylog 1 2 3 a b c ti

统计一组元素的个数:统计mylog中元素的个数(这个是统计的基数,基数就是去重后的元素个数),这个命令支持传入多个hyperloglog,这样就会直接统计所有传入集合的基数,而不用先merge了。
pfcount mylog

合并两个hyperloglog中的元素:合并mylog 和 mylog2 中的元素 到 newlog中(支持同时合并多个hyperloglog)
pfmerge newlog mylog mylog1

8.Bitmap位图

这也是一个组合词bit map,bit(位)是数据单位里的最小值,通常说的一个字节是8bit,map这里翻译成图,不过笔者感觉理解成键值对的map更合适,因为存储时都是以键值对的形式存储的,而且存储的键值对的值仅仅支持0和1,这也是bitmap名称的由来,这个0和1就是只占用1bit的大小,所以使用bitmap来存储数据是很高效的,bitmap适用于那些只有两个状态的场景,这种场景中使用bitmap效率很高。比如是否迟到、是否达到某个水平线、是否拥有某种物品,是否已婚等等可以适用的场景非常的多,而且bitmap也是只有三个命令,不过三个命令与其他数据类型的命令略有区别,其他数据类型的命令都是以关键字开头的,而bitmap中只有一个命令是bit开头,其他两个却是以bit结尾的。下面来看下她的三个命令:

往bitmap中设置值:设置sign中1(这个1表示位置)为 1(这个命令不支持一次插入多个值,必须一个一个插进去)
setbit sign 1 1
获取bitmap中的值:获取sign中第3位的值
getbit sign 3
统计bitmap中的数量(统计1的数量):统计sign中值为1元素的个数,0-1表示统计所有范围,默认也是统计所有
bitcount sign 0 -1

9.总结八种数据类型
认真了解了这八种数据类型以后会发现我们可以将redis中的所有数据类型基本划分为两大类,一类是主要用于数据存储的,另一类则更适合用于数据统计,主要用于数据存储的有string(存储单个值)、list(存储一列值)、hash(存储一组有关系的值到一个hash中)、geospatial(存储地理信息),而set、zset、hyperloglog、bitmap他们我们可以看到虽然也存储信息,但是更主要的功能还是用来做统计使用,比如使用set我们可以很方便的获取到不重复的数据,也可以很方便的获取多个set的交集、并集、差集,使用hyperloglog
则可以很方便的统计出一个或多个集合的基数,使用bitmap则很方便统计只有两种状态的数据。在知道了他们的特点后我们在日常的使用中才可以“知人善任”让他们可以更好地解决问题。

四、redis的事务与其他常见事务场景

1.Redis的事务

redis作为一种数据库是可以支持事务的,他既然支持事务是怎么支持的呢,我们都知道事务要有ACID的特性即:原子性、一致性、隔离性、持久性,下面先回忆介绍下这四个特性。

1)原子性:表示一个操作或者一组操作同时成功或者失败,只要他在执行其他都得等着他的执行结果,不可以中断这个操作。
2)一致性:表示事务执行完后,事务的处理结果应和预期的保持一致(个人理解)。
3)隔离性:隔离性说的是指多个用户并发访问数据库,开启了多个事务,事务之间应该是相互隔离的,互不影响(各个数据库默认都是不完全支持隔离性的,详见下面的隔离级别)。 
4)持久性:事务一旦提交对数据库的操作就应该是持久的,不能因任何原因而改变。

以上便是事务的四个特性了,简称ACID。那redis中的事务是怎么样的呢?redis的单条命令都是原子性的,但是redis的事物是不支持原子性的此外redis的事务也不支持隔离性,不支持隔离性应该很好理解,因为只有发起执行命令时所有的命令才开始执行,这样就不会有mysql中的脏读、幻读、重复读等现象。那不支持原子性该怎么理解呢?事务基本都是一组命令的集合,在mysql中的事务需要保证一组命令同时成功同时失败,不允许有其中一部分成功一部分失败,这就是原子性问题,但是redis中是是允许部分命令执行失败的,部分失败不会影响到其他的命令执行,也不会影响到整个事务的提交,所以说redis不支持事务的原子性,redis官方给出的解释是异常导致的事务失败属于编码问题,他们不负责处理,当然这里指的部分失败是运行时的异常,不能是命令错误,命令错误导致的异常会使整个事务都提交失败。

  • redis事务执行的三个命令
    ①multi:开启事务,multi多个的意思,应该是指支持多个命令的意思,当该命令被执行后接下来所有输入的命令都会进入到一个队列中,只有当使用exec提交指令后所有命令才会执行,或者使用discard命令丢弃事务后所有命令失效,这样一个事务才会结束。
    ②exec:提交事务,只有使用该命令后,事务中的一组操作才会真正开始执行,必须与multi配套使用,执行完该命令事务结束,
    ③discard:丢弃事务,使用该命令后会将因开启事务添加到队列中的所有指令全部丢弃,必须与multi配套使用,执行完该命令事务结束
    下面使用redis命令行简单提交个事务
    redis学习从练气到化虚_第2张图片

  • redis事务遇到异常的处理
    ①遇到编译异常,也就是命令错误,事务会提交失败,所有命令都不会执行
    ②遇到运行时异常,比如对字符串进行++操作,事务不会失败,只会失败异常的单条命令,事务整体正常提交
    这样redis的事务就说完了,可以发现redis的事务其实东西很少,就是开启执行、开启丢弃。没有其他的东西了,最主要的就是需要记住redis的事务不支持原子性、隔离性。来一起看看其他场景的事务机制,融汇贯通一下。

2. Spring的事务支持

Spring中提供了两种事务的支持一种就是我们常说的声明式事务,一种是编程式事务,两者各有优缺点,就笔者而言认为声明式事务更好一些,声明式事务采用AOP的形式对代码中的数据库操作进行管理,编程式事务则需要注入TransactionTemplate来进行开启和提交事务,这个是需要在代码中手动编写的对代码侵入比较高,无疑我们每个需要使用事务的代码可能都需要改动,但是也有一点好处就是编程式事务的控制粒度更细,而声明式事务只能控制到方法级别,但是很简洁,在实际开发中也是声明式事务使用居多。此外我们还要明确在spring中使用事务的场景,我们都知道常用的mysql都是默认支持事务的,所以单个表的操作我们在spring中完全可以不使用事务,因为有mysql在控制,那什么样的场景才需要控制事务呢?比如说常用的银行转账的例子,我们操作转账是一个表,记流水可能是另一个表,这种情况数据库的事务操作都只是保证一次操作的成功,不会保证你连续两次发送的操作一起成功和失败,所以说在一个业务里的多表操作时我们是需要事务来管理的。这里笔者主要总结下spring的声明式事务,在说声明式事务之前先回忆下事务相关的一些基本概念,如事务的传播机制、隔离级别、隔离级别引发的问题等,下面是Spring为事务管理提供的7种传播机制,先来看下其中传播机制(propagation)有哪些:

1REQUIRED(required):必须的意思,支持当前事务,没有事务就新建一个事务,反正就是必须得有事务。这是默认的选择
2SUPPORTS(supports):支持的意思,支持当前事务,没有事务则不会新建事务,通常查询方法配置这个。
3MANDATORY(mandatory):强制的意思,支持当前事务,没有事务就抛出异常,我就强制你有,没有我就不干了。
4REQUIRES_NEW(requires_new):必须是新的的意思,新建一个事务,事务本来就存在的话会被挂起。我就是使用新的,不用原来的。
5NESTED(nested):嵌套的意思,存在事务就执行一个嵌套事务,不存在就创建,与REQUIRES_NEW有些类似,但是不一样(一个嵌套,一个挂起)
6NOT_SUPPORTED(not_supported):不支持的意思,以非事务的方式执行,事务本来存在的话会被挂起。我就是不支持,有也不用。
7NEVER(never):绝不的意思,以非事务的方式运行,事务本来存在就报异常。我就是绝不容忍事务的存在。

以上便是事务的七种传播机制或者说叫传播行为,前三种是支持当前事务的,中间两种是支持新建事务的,最后两种则是不支持事务。一般需要使用事务的,我们直接是选择REQUIRED即可,比如增删改方法,不需要事务的则是SUPPORTS比如查询方法。事务的传播机制描述的是要不要使用事务,以及如何使用事务。事务本身有ACID的特性前面也介绍过了,但是在事实上因为考虑效率的问题数据库在对事务的支持上可能并不是严格的将ACID中的隔离性贯彻到底的, 在隔离性的实现上事务又划分出了几个等级,即:读未提交、读已提交、重复读、串行化。而因为隔离级别的不同又可能会出现脏读、不可重复读、幻读等问题。先来看下脏读、不可重复读、幻读:

1)脏      读:在一个事务中前后读取的同一条数据显示不一致,当前事务在查询时,另一个事务修改了数据且另一个事务还未提交,就会导致脏读
2)不可重复读:在一个事务中前后读取的同一条数据显示不一致,当前事务查询时,另一个事务更改了数据并提交,导致当前事务两次读取结果不一
              致,不可重复读描述的是另一个事务提交后导致的问题,脏读是指另一个事务还未提交。
3)幻      读:在一个事务中前后查询到的数据条数不一致,当前事务查询时,另一个事务做了插入和删除然后提交了,导致了当前事务前后查询数据  
               条数不 一致。幻读所描述的问题是数据的总量的变化。

介绍完了这三种可能出现的问题,再来看下解决这三种问题的策略,必须要说明的是下面的四种隔离级别都是数据库提供的,主流的数据库都是支持的比如mysql、oracle、sql server等等,和spring并没有关系,spring的声明式事务保证的是自己的一组业务逻辑符合事务,比如常用的DataSourceTransactionManager这个事务管理器,他是兼容spring与mybatis的事务机制的,一旦事提交失败,就可以通过mybatis(底层还是JDBC)将数据库的操作回滚,这里注意数据库不一定是单个事务,可能还是多个的(这里与数据库事务的持久性并不冲突仔细想)。下面来看下数据库支持的四种隔离级别。

1)Read uncommitted:读未提交,这是最低级别的隔离级别,啥问题也解决不了,使用这种机制会出现:脏读、不可重复读、幻读问题。
2)Read uncommitted:读已提交,这是oracle、sql server默认的隔离级别,可以解决脏读,解决不了不可重复读、幻读问题。
3)Repeatable Read:重复读,这是mysql的默认隔离级别,可以解决脏读、不可重复读,但是解决不了幻读的问题。
4)Serializable:串行化,这是隔离的最高级别,会将所有指令进行串行化,底层也是加锁,这会导致效率十分低下,很少用。

看到这是不是有个疑问?为什么重复读这个隔离级别可以解决不可重复读的问题,却解决不了幻读的问题呢?这就需要提一下数据库的存储引擎了比如我们常用的mysql他的默认存储引擎是InnoDB,InnoDB默认支持事务,且他操作表时是只锁定整行的,不会锁列也不会锁表,所以当一个事务查询时,另一个事务不可以更改当前行,但是却可以实现插入操作。这样就会有幻读的问题,不过锁行也只是默认的行为,这个行为会随着隔离级别的变更而变更的。
前面回顾了事务的ACID特性、事务在spring中的7种传播行为、事务在数据库中的四种隔离级别以及四种隔离级别所能解决的问题,前面一开始就说了spring支持编程式事务和声明式事务两种,这里主要是介绍redis时顺带回忆下spring的事务,所以这里只回忆声明式事务,下面看下一段声明式事务的代码:
redis学习从练气到化虚_第3张图片
可以看到完成一个声明式事务的编写就是这么简单,只需要三步如上图所示,
第一步:注入DataSourceTransactionManager这个事务管理器,注意他并不是唯一的还有其他的事务管理器,同时为事务管理器注入datasource数据源。
第二步:配置切面的通知,因为必须得知道这个切面切割哪些方法,这个就是通过advice来配置的,可以理解为通知这些方法遵循切面的规范。其中方法名的配置支持通配符,propagation是配置上面所说的事务的7种传播机制的,默认就是required,此外还可以通过rollback-for配置遇到什么异常回滚,通过no-rollback-for配置哪些异常不需要回滚等,此外支持只读的配置read-only,这个配置是个优化配置,使用在对数据库只有读的方法上,这样就可以根据这个配置对事务进行优化。当然这一步还有一个比较重要的点就是传入安全管理器。
第三步:配置切面,切面需要有切入点,各个切入点构成了一个切面,那怎么配置切面呢这里需要使用execution表达式来配置切面,他的语法也很简单如上图所示,第一个参数代表返回类型,通常使用通配符*来代替,第二个则是包名方法名和方法的参数,(…)表示方法的所有重载方法均切割。切面配置完了,还需要配置advisor这是一个织入配置,将切面和通知进行一个织入形成一个真正的AOP。
经过上面三步Spring的声明式事务就开发完成了,是不是很简单呢?不过这也不是唯一实现声明式事务的方式,我们还可以仅仅在配置文件中注入事务管理器,然后通过@Transactional注解来实现声明式事务的管理,这个注解可以使用在类上,表示里面的所有方法都需要事务,也可以使用在方法上表示当前方法需要事务,这个注解的属性支持与配置文件没什么区别。,此外事务一般使用在service层,事务是控制一个业务逻辑完整执行的保障,若是在dao层发生异常且捕获了,事务就不会受异常影响还会继续执行,这样是不对的,我们应该在dao层向上抛出异常,在service层来处理异常,那service应该怎么处理呢?service层我们可以选择捕获异常然后来定制与前端交互更友好的异常类,也可以简单粗暴直接抛出,但是不能catch住异常不让他继续抛了,这样是不对的,spring的声明式事务就是通过异常的捕获来实现回滚的,捕获了就不会回滚,一旦出现异常就会导致一个业务逻辑只完成了一半的情况,我们所使用的事务也就没有了意义,这是一个很容易被忽略的点,也是刚工作不久时很容易犯的错误。

3.Mybatis的事务支持

上面介绍了spring的事务管理,spring中通过DataSourceTransactionalManager来管理事务,Mybatis可以借助Spring中的DataSourceTransactionManager来做事务管理,这样我们就只需要在Spring中使用DataSourceTransactionalManager来做事务管理就行了,而不需要单独为Mybatis来提供事务的支持了。但是Mybatis是支持事务的,我们可以不用但是得知道,我们在指定数据源的时候可以指定Mybatis使用什么类型的事务,Mybatis默认使用JDBC的事务机制,JDBC的事务机制的伪代码如下:

connection.setAutoCommit(false);
try{
	connection.commit();
}catch(Exception e){
	connection.rollBack();
}

如上所示便是JDBC控制事务的方式,而Mybatis默认也是使用的JDBC的事务管理,当然我们也需要写一些代码,来实现事务管理,比如下面这样,其实也是很简单的ORM框架基本默认都是使用JDBC的事务管理,都是封装的老技术。不过我们已经有了在业务层的事务管理,这里其实是不需要在多一层的事务管理了,因为一旦发生异常,业务层是可以保证业务逻辑的完整性的,不会因为异常的发生而导致数据的不一致。
redis学习从练气到化虚_第4张图片

4.Mysql的事务支持

前面已经聊了Spring的事务支持和Mybatis的事务支持,事务最根本的支持还是数据库,这里只聊Mysql的事务,Mysql支不支持事务还得看他使用的存储引擎,目前Mysql默认使用InnoDB,她是支持事务的,而像MyISAM就不支持事务,那不支持事务怎么保证数据一致性呢,MYISAM操作数据库时默认为表加锁,也就是说对表的操作是串行化的,这样也是可以保证数据的一致性的,而InnoDB默认只会为行加锁,所以InnoDB需要事务机制,Mysql中的事务默认就是开启的,当我们执行任何的DML(数据操作语言如select、update、delete等)时默认都是事务开启的,执行完毕事务提交。那我们想要手动开启事务和提交事务怎么办呢?如下所示,我们只需要手动的start transaction就可以开启事务,使用commit提交事务,使用rollback回滚事务。如下所示:

  mysql> start transaction;#手动开启事务
  mysql> insert into t_user(name) values('pp');
  mysql> commit;
  mysql> rollback;

需要说的是事务一旦提交或回滚就会结束,不可以二次提交回滚,这里展示两个命令主要是为了观看,事实上提交后,再回滚是不支持的。到这里就介绍了开发中可以看到的大部分的事务支持,从redis的事务支持到spring的事务支持再到 mybatis的事务支持最后又说了mysql的事务支持,其中mysql与redis的事务支持操作起来其实都很简单,都是开启事务然后提交或者回滚就行,而mybatis的事务底层是JDBC的事务支持也没啥好说的,其实最主要的还是spring的事务管理与mysql的事务管理,我们日常开发中,在spring中声明事务然后就无需在mybatis中进行管理事务了,这样就可以保证一组业务逻辑的原子性问题,此外这些命令到了mysql数据库后,因为mysql默认支持事务,所以每个单条命令也会保证均支持事务。他们一起共同保证了一组业务逻辑的完美执行。

5.分布式架构中的事务

前面所说的都是单体应用下的事务机制,而当下很难再有单体应用,基本全部都是分布式应用场景,动辄几十个服务,这样我们该如何保证一组操作的事务呢,首先才是分布式事务呢?只有一次请求操作了多个数据库,这时候就需要保证所有数据库的操作同时成功同时失败,这就是分布式事务,核心是一个业务里面多个数据库的操作,而分布式服务若是连接的都是一个数据库,则不存在分布式事务的场景。
其实我们还可以这么理解,spring中的一个事务可能包含了多个表的操作,现在因为各个表的数据量太大,需要这几个表分散到不同的数据库,这种情况我们就需要去在一个事务中去操作不同的数据库。此部分未完待续。。。

6.redis整合jedis和springboot

redis是一个数据库,我们连接他肯定是需要一个工具的,就像我们连接关系型数据库需要使用jdbc一样,redis官方给我们提供了连接redis的工具,就是jedis,当然jedis是最原始的连接工具,目前也有部分项目在使用,springboot早起版本也是通过jedis来连接redis的,但是现在使底层已经使用Lettuce(生菜)来连接Redis了,他们本质上都是连接redis的工具包而已,可能lettuce性能更好吧,不然SpringBoot也不会把jedis替换掉。这里就不展示jedis的使用了,因为他是历史,我们必须了解,但是他的使用却和redis的客户端并无而知,所有redis的命令你在jedis中都能找到对应的方法。不过redis整合springboot这里也不做展示:推荐笔者写过的另一篇文章:SpringBoot整合Redis

五、reids面试高频点

1.Redis实现乐观锁

所谓乐观锁就是乐观的认为数据不会被修改,因此我不需要添加锁机制就可以实现并发的安全性,这就是乐观锁,比如java中的Reentrantlock就是一种乐观锁,而synchronized普遍被认为是一种悲观锁(不过synchronized在jdk1.6以后已经优化了,优化后优先使用CAS机制解决,CAS就是乐观锁的原理)。redis的乐观锁和java中的乐观锁思想差不多都是CAS(compare and swap),都是先获取,需要操作时比较,比较相同才会更新,因为更新是原子操作所以可以保证多线程的安全。这里需要说的是不仅redis和java中有乐观锁,像mysql、mybatis中也都有乐观锁,他们实现机制都是一样的,都是比较然后操作,但比较对象的可能并不是数据本身而已,比如mybatis中比较的是version。那redis的乐观锁使用什么机制实现呢?redis中使用watch(监视)命令来实现的,我们通过该命令可以监视一个key(watch可以一次监视多个key),一旦此时有其他线程进来把这个key变动了,那么这个事务中对于这个key的事务操作就会失败,同时失败后自动释放锁,(这里相当于手动执行unwatch)。因为使用的是乐观锁,所以其他线程是可以变更事务中的操作的对象的(这里主要是因为redis不支持隔离性)。redis的乐观锁可应用秒杀活动。下面来看下使用watch来实现的乐观锁的成功操作:

127.0.0.1:6379[3]> watch key1
OK
127.0.0.1:6379[3]> multi
OK
127.0.0.1:6379[3]> incr key1
QUEUED
127.0.0.1:6379[3]> exec
1) (integer) 3
127.0.0.1:6379[3]> 

来看一个使用watch乐观锁的失败操作:

127.0.0.1:6379[3]> set key1 3
OK
127.0.0.1:6379[3]> watch key1
OK
127.0.0.1:6379[3]> multi
OK
127.0.0.1:6379[3]> incr key1
QUEUED
127.0.0.1:6379[3]> exec
(nil)
127.0.0.1:6379[3]> 

2.Redis配置文件详解

配置文件是整个redis的核心,包括后面的RDB、AOF、哨兵、集群等等都需要更改配置文件,因此我们必须要弄懂配置文件,才能是更好的掌握所有的redis相关技术。redis的配置文件中各个配置对单位大小写都是不敏感的,如下:
redis学习从练气到化虚_第5张图片
在redis的配置文件中被划分出来了很多个区域,第一个就是includes,我们就按照这个顺序来理一理redis的配置文件

  • includes 多配置文件支持
    该区域用于引入其他的配置文件,也就是说redis支持像mybatis那样在一个配置文件中引入多个配置文件,这种支持很常见

    include /path/to/other.conf
    
  • network 网络配置
    该区域用于配置网络相关的参数,比如绑定的ip,redis的端口号等在这里配置,这里的ip绑定需要说的是默认是127.0.0.1,只支持本机访问,若是需要支持其他机器访问,可以配置成0.0.0.1也可以配置成统配,同时也支持多个ip的一起配置,下面列出了网络配置里面的重点部分,其余部分正常保持不变就好

    bind 0.0.0.0 #绑定ip
    bind 192.168.1.100 10.0.0.1  #绑定多个ip
    
    protected-mode no #保护模式,开启的话声明为yes即可
    port 6379 #修改redis占用的端口就是更改这里
    
  • general 通用配置
    通用配置顾名思义这里的配置都是适用于全局的,对整个redis起作用的,下面列出了一部分,第一个需要说一下什么叫守护模式呢?在java中垃圾回收线程被称为守护线程,守护线程是一种低调度优先级的线程,守护线程只有在服务器关闭才会停止,就是全程守护的感觉,而且Thread提供了设置守护线程的方法叫setDaemon有兴趣的可以去看看,这里的守护模式其实差不多,就是让redis以守护线程的模式运行,这样就不会在关闭redis-server的情况下推出redis了。

    daemonize yes #开始守护模式
    supervised no #用来管理守护线程的,不需要动
    pidfile /var/run/redis_6379.pid #用以存储reids的进程号的,我们使用ps -ef|grep redis 查到的就是这个值
    # debug (a lot of information, useful for development/testing)
    # verbose (many rarely useful info, but not a mess like the debug level)
    # notice (moderately verbose, what you want in production probably)
    # warning (only very important / critical messages are logged)
    loglevel notice # 日志级别管理,redis日志共有4个级别,默认使用notice不需要动,前两个日志打印会比较多,适用于开发环境
    logfile "" #日志的文件位置和文件名,使用空字符串表示使用标准输出
    databases 16 #配置数据库的数量,这个应该都知道redis有16个库,这里一般也不动
    always-show-logo yes #启动时是否显示logo,与spring的banner类似,这个配置没用
    
  • snapshotting 快照配置
    快照这个单词应该都不会陌生,这个单词在maven和jvm中会经常出现,这里的快照和jvm中有些类似都是指生成的文件,当redis在一定时间内发生了指定的次数的变更(key变更)那么redis会将信息进行生成一份快照,这个快照有两种形式一种是.rdb文件,一种是.aof文件,这也对应了redis两种不同的持久化策略,下面来看下这个的配置。

    save 900 1  # 表示900秒内发生了一次操作,就会触发持久化,若是多余1次则更会持久化了
    save 300 10 #300秒内发生了10次会触发持久化
    save 60 10000 # 60秒内发生了10000次会触发持久化,这些配置在真实场景中可能会被改变,有时业务是达不到6010000次的,那就可能300秒再持久花了,这很显然不是我们想要的,我们可以让他6010次就持久化,这样更能防止信息的丢失
    stop-writes-on-bgsave-error yes #配置持久化出错了redis是否要继续工作,默认就是是,无需更改
    rdbcompression yes #是否压缩rdb文件,默认是压缩的,压缩会消耗cpu一部分性能,若是cpu吃不消可以关掉
    rdbchecksum yes #保存rdb文件的是否是否进行错误的检查
    dbfilename dump.rdb #配置rdb文件的名称
    dir ./ #指定rdb文件保存的目录
    
    
  • REPLICATION 主从复制的配置
    这里的配置都是配置主从复制操作的,待完善。。。。。。。。。。。。

    
    
  • security 安全配置
    可以这这里配置redis的密码,redis默认登录是不需要密码的,我们使用requirepass命令可以查看密码,默认情况查出来就是空字符串,

    requirepass 1234abcd #设置redis的登录密码,真实开发中都需要密码,不可能随便让人直连的,所以这个还是很有用的,当然也可以通过命令来实现面的设置如:config set requirepass 1234abcd,使用config get requirepass 来查看密码.,使用auth 1234abcd来登录redis,使用配置文件设置密码与使用命令设置没有什么区别。
    
  • clients 客户端配置
    这里是主要设置最大的客户端连接数,其他也没什么可以设置的。

    maxclients 10000 # 客户端最大的连接数默认是10000 
    
  • memory manager 内存管理设置
    内存配置支持设置redis使用的最大内容,这个redis一般有自己的机制,我们不会强行干预,但也可以使用如下设置进行更改

    maxmemory <bytes> # 设置redis可以使用的最大内存,单位默认是字节
    
    1volatile-lru:只对设置了过期时间的key进行LRU(默认值) 
    2、allkeys-lru : 删除lru算法的key   
    3volatile-random:随机删除即将过期key   
    4、allkeys-random:随机删除   
    5volatile-ttl : 删除即将过期的   
    6、noeviction : 永不过期,返回错误
    maxmemory-policy noeviction # 内存达到最大时采取什么策略,这个策略总共有六种,默认是最严格的策略,永远不删除,内存达到最大时会返回错误
    
  • append only mode AOF模式配置

    appendonly no # AOF模式默认关闭,也就是说我们默认使用的都是RDB进行持久化模式
    appendfilename "appendonly.aof" # AOF模式下的默认文件名
    
    下面三个是处理同一种场景的,提供一个配置就行,默认开启的是每秒执行一次
    appendfsync everysec # AOF默认每秒进行一次持久化操作,这样最多导致1秒钟的数据丢失,通常情况下我们使用RDB完全可以解决问题,不需要使用AOF模式
    appendfsync always # 只要有数据变更就同步,这样的效率会比较低
    appendfsync no # 不执行同步数据,操作系统自己同步数据,这时候效率最快
    

3.持久化RDB操作

所有基于内存的数据库都需要持久化策略,不仅仅是redis,只要是基于内存就必须有持久化的策略,因为内存是断电即失的。在redis中支持两种持久化的策略,一种是RDB持久化,一种是AOF持久化,默认的情况下就是使用RDB进行持久化,RDB的配置就是上面 snapshotting 快照配置,这里的所有配置都是用来控制RDB持久化的,AOF持久化的控制都在append only mode中,默认全部关闭,我们一般都是使用RDB进行持久化,而不是使用AOF,先来看看RDB是如何进行持久化的
redis学习从练气到化虚_第6张图片

  • 如上图所示就是RDB进行持久化的一个流程:
    1)在snapshotting中的save 可以配置 多少时间内发生了多少次操作就会触发持久化,一般我们都是使用默认的那三个,当触发持久化机制时,主线程会fork一个子线程,来讲进行复制数据将数据写入到一个临时RDB文件中。
    2)当快照完全写入后,就会去替换掉原来的RDB文件,默认的RDB文件是dump.rdb,这个在snapshotting中也是可以配置的

  • RDB文件在哪里
    在snapshotting里面有个配置叫dir,上面介绍snapshotting时已经写过,默认是当前文件夹下,与redis-server同位置(为什么不是config的位置?因为redis-server会将配置文件加载过来,相当于是在redis-server所在的文件执行),rdb文件通常需要进行备份,以预防以外丢失rdb文件的情况。

  • 怎么替换RDB文件
    我们要是想要替换RDB文件,直接将其放在当前RDB文件的目录下即可,redis启动时会默认加载(dump.rdb)

  • 哪些场景会触发RDB操作
    1)执行save命令,手动执行一个save命令与配置文件中配置多少秒达到多少次执行save效果一样,此时会触发RDB操作。
    2)我们配置的save,多少秒内达到多少次操作也会触发RDB操作。
    3)执行fulushAll操作,也会生成RDB文件,清空数据库,redis默认生成一个rdb文件。
    4)关闭redis的时候默认生成一个RDB文件,但是若是使用kill命令杀死redis进程则不会生成RDB文件,所以我们应该绝对禁止使用kill来停止redis。

  • RDB持久化的优缺点
    1)RDB的优点:主线程操作数据不影响子线程的数据持久化,效率比较高,大数据量时数据恢复比较快。
    2)RDB的缺点:每次持久化都会有时间间隔,极端情况下可能会丢失最近一次的数据,当然这个时间可以配置,尽量减少风险。

4.持久化AOF操作

AOF是Redis支持的另一种持久化操作,RDB是记录的redis中的值,那AOF是怎么持久化数据的呢?AOF全称是Append Only File,追加文件的意思,AOF通过追加每次对redis执行的写操作来保存用户所有对redis进行的写操做,只要我们在redis中有写操作就会被保存到appendonlyfile.aof文件中,当我们想要恢复数据时,redis就会从新读取aof文件中保存的所有命令,当然了aof默认是关闭,我们若是想要使用AOF的话需要在append only mode模块进行修改,配置,在上面介绍redis的配置文件时已经介绍了这个配置,这里就不在重复赘述了。

  • 万一aof文件损坏了怎么办
    当aof文件损坏了的话,我们重启redis是不会成功的,此时我们可以使用redis-check-aof 来修复aof文件,注意这个修复会删除掉aof文件中不可以执行的部分,肯定会有数据丢失,但是相比丢失所有数据已经好了很多。
  • AOF持久化的流程图
    redis学习从练气到化虚_第7张图片
    上面的一套流程还是比较复杂的,我们不需要完全记住,只需要记住父进程fork了一个子进程,子进程负责根据存储快照将命令写入到临时aof,写入完通知主线程,主线程会将临时的命令写入redis的缓存,收到子进程完成的消息回去追加aof文件,最后会将新的aof文件替换为新的aof文件。
  • AOF的优点和缺点
    优点:每次修改都可以同步(不是默认的但是支持),文件的完整性会更好
    缺点:aof因为保存的是命令恢复起来会很慢,aof因为都是io操作,也比较慢,所以默认都是使用RDB。
  • 总结redis的两种持久化策略
    可以看到RDB是存储数据的方式保存起来,AOF是存储命令的方式存储起来,在大数据的情况下AOF恢复数据会很慢,但是他对数据的一致性支持更好,RDB在极端情况下回丢失最近一次保存的数据。

5.Redis订阅发布

Redis支持发布订阅的功能,这也是为什么说redis可以作为消息中间件的原因了,redis的提供的发布订阅功能其实不难,就是一个redis客户端(任何连接到redis的连接都可以看做是redis的客户端)负责发送消息,在redis中会有一个频道负责存储信息,当有客户端订阅了这个频道时,频道内的消息就会自动同步给订阅了的客户端,是不是和平时使用的消息中间件模式差不多,不过消息中间件比redis的消息处理要有优秀许多,毕竟人家是专业的,不过那也不是说redis的订阅发布就没了用处,比如说做个即时聊天室,我们会要求信息读写比较快,我们就可以使用redis的订阅发布来做,此外比如某些人关注了我,我发布了新的文章,其他人是下面来看下redis是怎么实现消息的发布到消息的订阅的。

  • 发布订阅的相关命令

    PUBLISH channel message # 发布一条消息到指定的频道,这个命令就比较重要了,是必须掌握的,此外若是频道不存在会直接创建该频道,将信息发送到该频道。
    
    SUBSCRIBE channel [channel ...] # 订阅一个或多个频道(命令中的[] 表示支持多个参数),订阅该频道后,该频道的信息会自动推送到当前客户端,需要说明的是,一旦当前客户端进入了订阅状态,那么当前客户端就只能执行订阅相关的命令了比如SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE和PUNSUBSCRIBE除了这些命令,其他命令一律失效。
    
    UNSUBSCRIBE [channel [channel ...]] # 退订指定的频道,这个没太多需要什么的就是退订,就像有些人关注了你然后又取关了你一样。
    
    # h?llo subscribes to hello, hallo and hxllo
    # h*llo subscribes to hllo and heeeello
    # h[ae]llo subscribes to hello and hallo, but not hillo
    PSUBSCRIBE pattern [pattern ...] #订阅匹配的频道,支持上面几种模式,比如我们订阅了h?llo,那么hello,hallo,hxllo的消息我们都可以接收到,?表示匹配任何一个单词,当然我们也可以写成?ello,这样就是匹配任何以ello结尾的四个字符的字符串频道,其他两种匹配看了上面的例子应该都可以理解,就不一一介绍了。
    
    PUNSUBSCRIBE [pattern [pattern ...]] # 这个命令与上面的正好是反的,上面是订阅匹配的频道,这里是退订匹配的频道,都可以理解为是批量操作。
    
    PUBSUB subcommand [argument [argument ...]] # 这是一个组合命令,比如 PUBSUB channels 列出所有活跃的频道,这个命令后面也可以跟h?llo 这样检查活跃的频道的范围就不不是全站,而是符合要求的频道了,除了channels此外还支持其他的子命令如numsub返回某个频道的订阅数等。
    
  • 使用两个客户端实现发布订阅的功能
    上面已经介绍了发布订阅的相关命令,知道了这些命令那么我们就可以使用发布消息的命令去实现redis的发布订阅了(使用jedis、lettuce等也是类似的操作),如下图所示左边的客户端订阅了studentPro这个频道,右边在往studentPro这个频道发送消息后,左边立马就接收到了这个消息“hello,studentPro”。这样发布订阅的功能我们就实现了,redis的发布订阅就是这么简单。
    redis学习从练气到化虚_第8张图片

  • redis订阅发布的应用场景
    因为现在消息中间件技术已经非常成熟比如想要处理大数据就用kafka,想要生态好可以使用RabbitMQ,想用阿里系可以还是RocketMQ,这就导致了redis的订阅与发布肯定不会作为日常项目中消息中间件的首选,功能也不如前面几个强大,但也不是没有redis的生存之地,比如做个小型的消息订阅,网站上的即时聊天,站内消息推送等等我们都可以使用redis来完成,这样不仅快而且操作开发也方便。

  • 发布订阅的底层实现
    redis通过维护一个pubsub_channels字典来存储频道信息与订阅这信息,这个字典中每一项可以理解为一个键值对,键就是一个频道的名称,值是所有订阅了这个频道的链表。当有消息发布时,根据频道名称去查找key,找到对应的key以后,将消息发送给链表中的每一个客户端。这就是redis发布订阅的底层原理。

6.Redis的主从复制

相信使用过redis的小伙伴对redis的主从复制应该都不陌生,因为只要使用redis那就肯定会用主从复制(Master-Slave),为什么叫主从复制而不叫从主复制呢?这是因为redis只支持主节点到从节点的数据复制,主节点只用来写,而从节点只用来读,采用一主多从的模式也就是一个主节点可以有多个从结点但是一个从节点只能有一个主节点。我们在使用redis的时候大部分都是读操作,因此我们在配置主从模式时,通常都是一个主节点多个从节点,那要是主节点挂了呢?就不写了吗,当然不是,使用主从复制就必须得说哨兵模式,哨兵模式就是为了在主从复制中主节点意外挂掉以后从新选举主节点用的,在下一节会详细介绍哨兵模式,下面一起看下主从复制

  • 主从复制 有哪些优点
    1)数据冗余:读写分离、主从复制保证了每个结点都有所有数据,数据做了多份冗余就很难丢失了。
    2)故障恢复:单机的redis挂了以后服务肯定会受很大影响,主从复制中即使主节点挂了,利用哨兵模式也可以很快从新选举主节点,所以主从模式至少需要三台服务器。
    3)负载均衡:一主多从的模式,保证了大量的读操作不会打在一台服务器上,会大大降低服务器的压力。
    4)高可用基础:因为有了主从复制,才有了哨兵模式和集群的出现。

  • 主从复制的配置
    默认情况下所有结点都是主节点,所以我们必须配置声明哪些是从节点,事实上我们也只需要配置从节点,因为默认都是主节点的我们可以使用info replication查看当前结点的主从信息,下图中是默认的信息,标注了当期结点是主节点,从节点个数是0:
    redis学习从练气到化虚_第9张图片
    当有从节点时默认使用主从复制模式,此时就是主节点只能写,从节点只能读了。那从节点应该怎么配置呢?上面再说配置文件时其实已经提到过主从复制的配置了,就是replication这块的配置项,下面我们就来一步步来配置主从复制:

  • 配置主从复制
    1)将redis.conf文件复制出来两份redis-1.conf、redis-2.conf,更改新增配置文件端口为6380、6381,并更改rdb的默认文件名dump-1.rdb,dump-2.rdb,其次需要更改pid文件(上面解释配置文件信息时提到过),最后要记得还要更改logfile为80.log,81.log,并启动redis服务,这样我们就在一台机器上搭建了三个redis服务。备注:这里也可以使用命令实现slaveof 127.0.0.1 6379,但是使用命令不是永久的,从机一旦挂掉从起后就会变成主机模式,所以我们在真实环境肯定还是使用配置文件。

    # 下面展示下6380 这个配置文件的修改
    port 6380 #修改默认的端口号
    pidfile /var/run/redis_6380.pid # 修改默认的pid文件,不存在直接创建,
    logfile "80.log" # 修改默认的日志文件,不存在直接创建,注意创建用户的权限
    dbfilename dump80.rdb # 修改rdb默认的持久化文件,不存在会直接创建
    

    2)找到replication配置模块更改如下配置

    replicaof <masterip> <masterport> # 这个配置是用来声明当前结点是谁的从节点,默认不打开该配置,比如可使用如下的配置来配置刚刚启动的两台redis
    replicaof 127.0.0.1 6380 # 修改从节点1的配置文件redis-1.conf为此
    replicaof 127.0.0.0 6381 # 修改从节点2的配置文件redis-2.conf为此
    

    配置完成后重启redis服务,使用info replication查看三台redis服务器,就可以看到6379服务器可以看到多了两台从机,而6380和6381都从主机变成了6379的从机。其余replication还支持配置主机密码等配置项,其余配置一般使用默认即可。

  • 验证是否配置成功
    怎么验证我们配置的主从模式是否成功了呢,我们可以直接在主机和从机上使用info replication命令,主机上使用该命令会展示出他所有的从机,从机上使用该命令会展示他的主机是谁,当然无论主机还是从机也都会标注他的身份是主机还是从机,展示如下,这样我们就配置完成了主从模式。
    redis学习从练气到化虚_第10张图片

  • 验证主从复制是否可用
    配置主从模式就是为了读写分离的,我们测试下是否如我们所想,我们在主机上随便设置一个key查看两个从机是否可以读取到这个key的值,如下图,我们可以看到主机上设置值以后从机上是可以顺利获取的,说明主从模式我们配置的没有问题。
    redis学习从练气到化虚_第11张图片

  • 验证从机是否可以执行写操作
    主从模式中主机虽然是用来写的,但是他也可以执行读操作,但是从机是不能执行写操作的,因为redis只支持数据的单向流动,即从主节点到从节点的数据复制,那么我们来验证下从结点是否是真的不可以实现写呢,如下所示我们在从节点执行了写操作,然后就报错了,证明从节点确实是不支持写入操作的。
    redis学习从练气到化虚_第12张图片

  • 主从复制里的全量复制、增量复制
    从上面的验证我们可以发现这个主从配置已经没有问题了,那我们主节点数据是如何被复制到了从节点呢,是以什么策略复制过去的呢,这就必须得说全量复制和增量复制了。当一个从节点第一次连接到主节点时,主节点会将全部的信息都同步到从节点这是全量复制,若是断开重连也会全量复制,在正常情况下产生的数据,主从节点使用增量复制的方式将新增的数据同步至从节点。

  • 主从模式的另一种配置方式
    上面我们配置的主从模式配置的是一主二从,其实我们还可以配置如下这种主从复制的模型,中间的从节点又是后面的主节点,但是这么配其实也没啥用,只是提供个思想,不过在这种情况中中间节点还是从节点,无论是这种模式配置的主从复制还是上面的配置,我们都不可能直接使用的,因为在实际的场景中我们必须使用哨兵模式,这样才能保障服务的相对稳定和安全。
    redis学习从练气到化虚_第13张图片

7.哨兵模式详解(sentinel)

上面说了主从复制读写分离,在这个模式中主节点只有一个而从节点可以有很多个,那万一主节点挂了怎么办,在哨兵模式出来之前我们可以使用命令手动选举一个主节点,我们只需要在一个从节点上告诉他你没有主节点了即可,他就会成为新的主节点,命令是:slaveof no one,但是当哨兵模式出来以后这种手动选举的很显然并不会合适了。

  • 什么是哨兵模式
    哨兵模式是通过一个独立的进程来不断的向主节点和各个从节点发送请求来检测主从节点是否是可用的,在收不到响应信息时哨兵就会认定为这个服务已经不可用,此时通过选举在多个从节点中选举出一个新的主节点,以保障服务的可用性。
    redis学习从练气到化虚_第14张图片
  • 多哨兵模式
    当只有一个哨兵时,服务也是不稳定的,当哨兵服务挂掉以后将不会有新的主节点被选举出来,因此在实际中哨兵也需要配置多个,哨兵之间也会通信,当一个哨兵挂了以后其他哨兵依然可以正常的选出一个主节点,以保证服务的可用性,多哨兵模式时的图可以参考如下
    redis学习从练气到化虚_第15张图片
  • 哨兵模式的配置
    1)配置哨兵配置文件sentinel.conf,在该配置文件中写入如下配置,多个哨兵的话每个哨兵也是这么配置
# sentinel monitor 是固定命令
# [master-group-name] 哨兵的名称,可以随便取i
# ip 主机的ip
# port 主机redis服务的端口
# quorum 这个参数表示需要几票才能选举出一个新的主节点,这里我们只配置了一个哨兵,所以最大只能写1,当我们有三个哨兵时,这个参数可以设置成,1,23都是可以的
sentinel monitor [master-group-name] [ip] [port] [quorum]

示例展示:
sentinel monitor sentinelOne 127.0.0.1 6379 1

此外还有一些其他的配置,用以支持哨兵模式,不过其他命令直接使用默认的形式也是没有问题的,我们需要配置的唯一项就是告诉哨兵的配置文件谁是主机,这样当主机宕机后,哨兵就可以根据这个主机的从机从新选举出一个主节点。

  • 测试哨兵模式是否起了作用
    我们模拟下以外情况,使用kill命令杀死主节点,若是哨兵可以在短时间内选举出新的主节点说明哨兵模式运行是正常的。笔者杀死6379端口号后,6381成功变成了主节点,如下图所示,表示我们的哨兵模式是没有问题的,必须说的是当主节点因为意外情况导致宕机后,我们将他重新启动后主节点还是主节点但是已经不是原来主从模式的主节点了,这个主节点是redis的默认行为,若是想将它加入到主从模式中,他就只能作为从节点加入进去了,而且新选举出的主节点和从节点的信息都是持久化到配置文件中的,是持久化的不会随便更改。
    redis学习从练气到化虚_第16张图片
  • 哨兵模式优点
    哨兵模式是多个哨兵服务,一个主节点和至少三个从节点共同构建出来的redis的服务集群,哨兵模式与主从模式对比无疑是更优秀的选择,他比主从模式更智能,可以更自主的切换主节点,进行故障转移,哨兵模式就是主从模式的进化版,我们真实环境也都是使用哨兵模式。
  • 哨兵模式缺点
    redis不支持在线的扩容,除非是横向扩容,集群容量一旦到了上限就会很麻烦,所以要求我们在设计redis集群时就需要考虑到系统的高可用性,此外哨兵模式配置其实支持的也不少,我们若是配置完了也很麻烦,不过也不必烦恼,这些都是运维的活,作为开发这些不必完全掌握,但是我们需要了解这些原理,知晓简单的配置,因为很可能测试环境需要我们自己配置,所以懂还是要懂的。
  • 哨兵模式是如何选举主节点的
    说哨兵模式,那我们必须要知道的是哨兵是如何选举出新的主节点的(因为面试会问),假设在一个哨兵模式中,主节点因为意外宕机了,哨兵1监控到了这个结果,其实并不会立马进行failover过程(故障切换也就是从新选举)当前一个哨兵监测到主节点不可用属于主节点的主观下线,当其他哨兵节点也检测到主节点也下线时会发起一个投票机制,这个叫客观下线,此时才会进行真正的failover进行故障转移,从而选举主一个新的主节点。

8.分布式锁

在一些需要串行化执行的程序中,我们是需要为程序加锁的这样可以保证不会因为线程的并发而导致数据出现的紊乱。在单体应用架构中很好实现,我们使用synchronized或者ReentrantLock均可以实现这样的效果,但若是在一个分布式场景中这种处理肯定就不可以了,假设同一时间有四个请求到达了两台服务器,每个服务器都收到了两个请求。若是程序使用的是ReentrantLock,则一台服务器中的两个请求会串行化执行,这在一台服务器中没有问题,但是另一台也是收到两个请求,此时就可能出现两台服务器同时执行一个请求的情况,造成数据失真。这就是分布式场景下的并发问题,因此只要我们是分布式服务,肯定有场景需要使用分布式锁,比如开个定时任务,若是多台服务器一起执行,肯定会出问题,造成数据的重复处理,数据很可能会失真,那此时我们就要为定时任务加个分布式锁,只有获取了分布式锁才可以继续执行。那怎么加分布式锁呢,笔者这里总结了redis中常用的两种方法来实现分布式锁,如下:

  • 使用redis自带命令实现分布式锁:setnx
    setnx命令上面介绍string数据类型的命令时已经说过了,没有记清楚的可以翻到上面看一下哈,笔者在这里会先用setnx来设计分布式锁,然后根据这个锁的隐患来分析可能产生的问题然后一步步完善这个锁, 最后实习在分布式场景中的高可用的分布式锁。下面我们就来一步一步来完成这个分布式锁:
    1)使用setnx实现简单的分布式锁…未完待续
  • 使用redission实现分布式锁

9.缓存穿透

  • 什么是缓存穿透
    缓存穿透就是未使用缓存中的信息,请求直接打到了DB上,这样肯定会增大数据库的压力,在很多请求同时进来时,会直接撑爆数据库,这种现象被称为缓存穿透,缓存穿透是redis中没有对应的缓存,请求直接大量打到了数据库上而导致的数据库压力大增,可能会导致数据库的崩溃。
  • 如何解决缓存穿透
    缓存穿透的概念不难理解,就是未合理的设置缓存而导致的问题,此时要求我们必须对缓存进行合理的设置,不能让高并发的场景中的请求直接打到数据库中,必须从缓存中查询这些信息,此外若是大量请求同时进来缓存没有数据库也没有可能也会造成类似问题,此时可以通过第一次去数据库查询未查到,就把一个空的缓存放到redis中,这样其他请求就不会直接进入到数据库中了。

10.缓存击穿

  • 什么是缓存击穿
    缓存击穿与缓存穿透有些类似,都是短时间内大量请求打到了数据库上,但是缓存击穿是因为redis的键失效而导致的大量请求直接访问到了数据库,在短时间内数据库的压力大增。
  • 如何解决缓存击穿
    1)方式一简单粗暴,我们直接让热点信息设为永久,这样就不会有缓存击穿的问题出现。
    2)方式二进行加锁处理,加锁无疑会降低服务的性能,但是与出现缓存击穿的 问题无疑是稍微能令人介绍一些的。

11.缓存雪崩

  • 什么是缓存雪崩
    缓存雪崩是比缓存击穿更严重的问题,当在一段时间内大量的key集体失效了或者说缓存服务宕机了,从而导致了大量的请求全部打到了数据库中,此时相当于缓存已经失去了作用,所有的请求一下子全到了数据库这种情况数据库肯定是扛不住的,就会导致数据库的服务宕机从而影响到整个集群的可用性。
  • 如何解决缓存雪崩
    缓存穿透,缓存击穿和缓存雪崩都是很严重的问题,这些就问题我们在生产上都应该是必须要避免的,同时他们也都是面试的高频考点,笔者也被问到过,缓存雪崩作为最为严重的一种问题最可能出现的原因其实就是服务器的宕机,那我们要如何避免这种情况呢:
    1)我们可以为redis搭建集群,使用哨兵模式+集群这样可以最大限度的避免redis服务宕机的情况
    2)限流降级,既然是因为大量的请求短时间内都到了数据库,那我就不让你在短时间进去,我做个加锁操作,只要缓存有效就不会影响服务性能,只有在缓存大量失效时,才会进入到加锁的流程中。
    3)数据预热,所谓数据预热就是对某些可能出现的热点场景提前进行人为操作将数据加载到缓存中,避免被大量请求同时打进来,同时对热点信息进行失效时间均分分布,避免出现集体失效的场景。

你可能感兴趣的:(redis,redis)