Redis 从入门到精通

目录

什么事Redis?

常用的基础命令

redis压力测试工具

​编辑

redis基础的知识

redis是单线程的

五大基本数据类型

String(字符串)

List(列表)

Set(集合)

Hash(哈希)

Zset(有序集合)

三种特殊数据类型

geospatial                【地理位置】

hyperloglog    【基数统计】        

bitmap            【位图】

事务

Redis配置文件

Redis持久化

RDB

AOF

Redis 订阅/发布模式

Redis主从复制

Redis 哨兵机制

 Redis缓存模型及分析

缓存穿透及解决方案

缓存击穿及解决方案

缓存雪崩及解决方案

基础API与jedis详解

SpringBoot集成Redis操作

问题: 为什么redis存入中文时,在java中是正常的,在redis客户端里面出现了乱码?

​编辑

如何解决序列化产生乱码的问题?

自定义封装RedisTemplate:

Redis 的实战分析

锁(Redission)


  • 什么事Redis?

        先来看看官网是怎么说的:

        Redis是一个开源(BSD许可)的内存数据结构存储,用作数据库、缓存、消息代理和流引擎。Redis提供了字符串(String)、哈希(Hash)、列表(List)、集合(Sets)、带范围查询的排序集合(Zset)、位图(Bitmap)、超日志(hyperloglog)、地理空间索引(Geo)和流等数据结构。Redis具有内置复制、Lua脚本、LRU逐出、事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster自动分区提供高可用性。

        您可以对这些类型运行原子操作,如附加到字符串;增加哈希中的值;将元素推送到列表;计算集合交集、并集和差集;或者获得排序集合中排名最高的成员。

        为了获得最佳性能,Redis使用内存数据集。根据您的使用情况,Redis可以通过定期将数据集转储到磁盘或将每个命令附加到基于磁盘的日志中来持久化数据。如果您只需要功能丰富的网络内存缓存,也可以禁用持久性。

        Redis支持异步复制,具有快速的非阻塞同步和自动重新连接,并在网络拆分时进行部分重新同步。

其实总结成一句话:Redis就是一个基于内存的高性能缓存级别的非关系型数据库。

  • 常用的基础命令

        先将redis的配置文件 redis.conf 复制到 /etc/redis 目录下,在该目录下简单配置一下redis的配置文件,然后将名字改掉,最后启动Redis!

# ls                                                //这里可以编写各种不同的配置文件
redis79.conf  redis80.conf  redis81.conf  redis.conf  sentinel.conf  sentinel.conf.dpkg-old
# redis-server redis80.conf             //以各种不同的配置文件启动service    

# ps -ef|grep redis                            //查看redis进程
redis        906       1  0 04:52 ?        00:00:21 /usr/bin/redis-server 127.0.0.1:6379
root       15751       1  0 08:48 ?        00:00:00 redis-server 127.0.0.1:6380
root       15863    8786  0 08:50 pts/0    00:00:00 grep --color=auto redis

        先认识一下Redis的一些基础命令,如:get 、set 、keys 等等!

GET  [KEY]                获取对应key的值

SET [KEY] [VALUE]      设置对应KEY的VALUE值

KEYS *                        显示所有KEY

redis-cli             启动用本地客户端取去链接本地的redis服务,及127.0.0.1:6397

redis-cli -h [host] -p [port] -a [password]         host:地址  、port:端口号  、passworld:密码

键相关的命令:      

  1. DEL [KEY]                当 KEY 存在时就删除KEY
  2. DUMP [KEY]             序列化给定的KEY,并将序列化的KEY返回过来。
  3. EXISTS [KEY]         检查给定的KEY是否存在。
  4. SETNX [KEY] [VALUE]        当给定的KEY存在的时候就不做任何操作,当给定的KEY不存在的时候就创建一个KEY并将VALUE值赋值给KEY 这时就相当于 SET [KEY] [VALUE] 命令;该条命令就是实现乐观锁的底层实现方式。
  5. SETEX key seconds value       设置过期时间
  6. EXPIRE [KEY] [SECONDES]          给指定的KEY设置过期时间,时间单位是秒。
  7. EXPIREAT [KEY] [TIMESTAMP]        EXPIREAT 命令和 EAXPIRE 命令差不多,都是给 KEY 设置过期时间。区别在于 EXPIREAT 命令接收的时间参数 TIMESTAMP 是UNIX时间戳。
  8. PEXPIRE [KEY] [MILLISECONDS]        给指定的 KEY 设置过期时间,时间参数 MILLISECONDS 是毫秒。
  9. PEXPIREAT [KEY] [MILLISECONDS-TIMESTAMP]        给指定的KEY设置过期时间的时间戳,以毫秒计。
  10.  KEYS [PATTERN]        查找所有符合给定模式 PATTERN 的KEY。
  11. MOVE [KEY] [DB]        将当前数据库的 KEY 移动到指定的数据库DB当中.。
  12. PERSIST [KEY]        以毫秒为单位返回 KEY 剩余的过期时间。
  13. PTTL [KEY]        以秒为单位返回指定 KEY 的剩余过期时间。
  14. RANDOMKEY        从当前数据库随机返回一个 KEY 。
  15.  RENAME [KEY] [NEWKEY]        修改指定 KEY 的名字。
  16. RENAMENX [KEY] [NEWKEY]        仅当 NEWKEY 不存在的时候,将 KEY 的名字改为 NEWKEY 该命令的性质和 SETNX [KEY] [VALUE]  差不多。
  17. SCAN cursor [MATCH pattern] [count]        迭代数据库中的数据库键。
  18. TYPE [KEY]        返回 KEY 所存储的 VALUE 值得类型。
     
  • redis压力测试工具

redis-benchmark      【表格来自菜鸟教程】

序号 选项 描述 默认值
1 -h 指定服务器主机名 127.0.0.1
2 -p 指定服务器端口 6379
3 -s 指定服务器 socket
4 -c 指定并发连接数 50
5 -n 指定请求数 10000
6 -d 以字节的形式指定 SET/GET 值的数据大小 2
7 -k 1=keep alive 0=reconnect 1
8 -r SET/GET/INCR 使用随机 key, SADD 使用随机值
9 -P 通过管道传输 请求 1
10 -q 强制退出 redis。仅显示 query/sec 值
11 --csv 以 CSV 格式输出
12 -l(L 的小写字母) 生成循环,永久执行测试
13 -t 仅运行以逗号分隔的测试命令列表。
14 -I(i 的大写字母) Idle 模式。仅打开 N 个 idle 连接并等待。

root@dhy:~# redis-benchmark -h localhost -p 6379 -c 100 -n 100000

====== SET ======
  100000 requests completed in 0.52 seconds
  100 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 900 1 300 10 60 10000
  host configuration "appendonly": no
  multi-thread: no

97.88% <= 1 milliseconds
99.72% <= 2 milliseconds
99.96% <= 3 milliseconds
100.00% <= 3 milliseconds
190476.20 requests per second

......

测试数据解析

Redis 从入门到精通_第1张图片

  • redis基础的知识

        redis默认有16个数据库,这里我们可以在redis的配置文件中看到,默认使用第0个数据库。

 在配置文件可以看到,我们可以使用 select 命令来切换数据库。

127.0.0.1:6379> select 3                #切换至数据库3
OK
127.0.0.1:6379[3]> dbsize               #查看当前数据大小
(integer) 0

清空当前数据库: FLUSHDB

127.0.0.1:6379[3]> set name "greatly monster"
OK
127.0.0.1:6379[3]> keys *
1) "name"
127.0.0.1:6379[3]> get name
"greatly monster"
127.0.0.1:6379[3]> FLUSHDB                #清空当前数据库数据
OK
127.0.0.1:6379[3]> keys *
(empty array) 

清空所有数据库数据: FLUSHALL

127.0.0.1:6379[3]> set name "greatly monster"
OK
127.0.0.1:6379[3]> SELECT 0
OK
127.0.0.1:6379> keys *
1) "mylist"
2) "k1"
3) "key:__rand_int__"
4) "counter:__rand_int__"
5) "myhash"
127.0.0.1:6379> FLUSHALL                #清空所有数据库
OK
127.0.0.1:6379> KEYS *
(empty array)
127.0.0.1:6379> SELECT 3
OK
127.0.0.1:6379[3]> KEYS *
(empty array) 

相关命令:

  1. SELECT [index]                切换数据库
  2. DBSIZE                             查看当前数据库大小
  3. FLUSHDB                         清空当前数据库数据
  4. FLUSHALL                        清空所有数据库数据
  • redis是单线程的

        redis是单线程的,redis是基于内存操作,CPU不是Redis的性能瓶颈,Redis的瓶颈是和机器的内存和机器带宽。

Redis为什么单线程还能这块?

        误区1:高性能的服务器一定是多线程的?

        误区2:多线程(CPU上下文切换!)一定比单线程效率高?

        该问题核心在于:redis是将所有数据都放进内存中操作,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!!!),对于没有上下文切换效率就是最高的!多次读写就是在一个CPU上的,在内存情况下,这就是最好的解决方案。

问题:听说6.x之后的Redis版本引入了多线程?那redis还是单线程的吗?

        上面说道的redis是单线程的主要是指其redis的工作线程是单线程的,然而这里引入的多线程是不在工作线程之列的,redis为了保证工作线程安全,工作线程还是使用的是单线程。redis引入的多线程是用在处理异步删除、数据同步、数据持久化等操作,从而提升redis的性能!其实早在redis 4.x版本之后就开始慢慢的引入线程来处理异步删除、持久化、刷盘等操作。

        这里多线程我们也可以在其配置文件中可以看到多线程相关的一个配置(这里就一6.x版本的redis来列举说明,本文之后的内容都将默认redis是6.x之后的版本),在配置文件中,多线程的配置默认是关闭的,若需要使用需要手动开启!

  • 五大基本数据类型

  • String(字符串)

  1. APPEND [KEY] [VALUE]        #在指定的 KEY 后面追加字符串 VALUE,如果KEY不存在就新建一个KEY,就相当于SET [KEY]
  2. STRLEN [KEY]                        #获取指定 KEY 的字符串长度
  3. INCR [KEY]                             #给指定的KEY进行+1操作 { 自增1 }
  4. DECR                                      #给指定的KEY进行-1操作 { 自减 1 }
  5. INCRBY [KEY] [INCREMENT]  #给指定的 KEY 加上 INCREMENT 步长
  6. DECRBY key decrement          #给指定的 key 减掉 decrement 步长
  7. GETRANGE key start end        #截取字符串,从下标start开始到下标end结束
  8. SETRANGE key offset value    #从下标offset开始替换字符串
  9. MSET {[KEY] [VALUE] [KEY] [VALUE]...}    #同时设置多个 key value
  10. MSETNX {[KEY] [VALUE]....}           #同时SET多个KEY 并且当KEY不存在时才设置,该命令是保证原子性的。
  11. MGET {[KEY] [KEY] ...}    #同时得到多个KEY
  12. GETSET key value        #如果存在值,获取原来的值,并设置一个新的值。

String类的使用场景:value除了是String字符串,还可以是数字!

  • 计数器
  • 统计多单位的数量
  • 粉丝数
  • 对象存储!
  • List(列表)

        基本的数据类型,列表!在redis里面我们可以把list当做栈、队列、阻塞队列!所有的list命令都是以L开头的命令。

  1. LPUSH key element [element ...]                        #将一个值或多个值插入到列表的头部
  2. LPUSH key element [element ...]                        #获取list中的值
  3. RPUSH key element [element ...]                       #将一个或好多个插入到列表的尾部
  4. LPOP key                                                            #移除列表的头部元素
  5. RPOP key                                                            #移除列表尾部元素
  6. LINDEX key index                                                #获取列表对应下标的元素
  7. LLEN key                                                              #返回列表的长度
  8. LREM key count element                                     #移除列表中指定个数的值
  9. LTRIM key start stop                                            #根据下标截取指定的长度,这个list已经被改变了,截断了只剩下截取的元素
  10. RPOPLPUSH source destination                          #移除列表的最后一个元素,并将最后一个元素移动到新的列表中。
  11. LSET key index element                                        #设置列表中指定下标的值
  12. LINSERT key BEFORE|AFTER pivot element       #将某一个具体的value插入到列表中某个元素的前面或者后面。

应用场景:

  • 消息排队!
    • 消息队列(LPUSH  RPOP)左边插入,右边取出
    • 栈(LPUSH  LPOP)左边插入,左边取出
  • Set(集合)

        set中的值不能重复,set中的命令都是s开头的。set是无需不重复集合!

  1. SADD key member [member ...]                #向集合中添加一个或多个元素
  2. SMEMBERS key                                        #查看集合中的所有元素
  3. SISMEMBER key member                         #判断某一个元素是否在集合中
  4. SCARD key                                                 #获取集合中的元素个数
  5. SREM key member [member ...]                 #移除集合中的指定元素
  6. SRANDMEMBER key [count]                      #随机获取集合中的一个或指定个数元素
  7. SPOP key [count]                                        #随机弹出一个或多个元素
  8. SMEMBERS key                                          #随机移除一个元素
  9. SMOVE source destination member            #将集合中指定的元素移动到另一个集合中
  10. SDIFF key [key ...]                                        #求多个集合的差集
  11. SINTER key [key ...]                                     #求多个集合的交集
  12. SUNION key [key ...]                                     #求多个集合的并集

 应用场景:

  • 共同关注
  • 好友推荐
  • 抽奖
  • 共同爱好
  • ...........
  • Hash(哈希)

        Map集合,key-map!这个时候这个值是一个map集合。Hash的相关命令都是以H开头的。

  1. HSET key field value [field value ...]           #向指定hash里面set一个或多个具体的key-value
  2. HGET key field                                            #获取指定hash里面指定key对应的value
  3. HMSET key field value [field value ...]         #向指定hash里面set一个或多个具体的key-value
  4. HMGET key field [field ...]                           #获取指定hash里面的一个或多个key对应的value
  5. HGETALL key                                             #获取指定hash里面的所有值

  6. HDEL key field [field ...]                               #删除指定hash里面指定的一个或多个key字段,其对应的value也被删除了

  7. HLEN key                                                     #获取指定hash中的key-value个数

  8. HEXISTS key field                                       #判断指定hash里面的指定字段是否存在

  9. HKEYS key                                                  #获取指定hash中的所有字段名

  10. HVALS key                                                   #获取指定hash中所有字段值

  11. HINCRBY key field increment                      #给指定hash中的指定字段进行自增的操作

  12. HSETNX key field value                               #和setnx一样

String里面的命令在Hash里面基本上都可用。

Hash的应用场景:

  1. 存储对象
  • Zset(有序集合)

        在set的基础上增加了一个值,试集合变得有序  set k1 v1 ==> zset k1 score1 v1 ,zset就是用这个新添的值来做排序,使得无序集合变成有序集合。

  1. ZADD key [NX|XX] [CH] [INCR] score member [score member ...]     #向有序集合中添加一个或多个值
  2. ZRANGE key start stop [WITHSCORES]           #遍历zset
  3. ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]     #排序 从小到大
  4. ZREVRANGE key start stop [WITHSCORES]          #排序,从大到小
  5. ZREM key member [member ...]                #移除指定的元素
  6. ZCARD key                                               #获取有序集合中的元素个数
  7. ZCOUNT key min max                           #通过指定区间获取zset中的元素个数

 应用场景:

  • 存储权重数据
  • 如:排行榜、top榜等
  • 成绩、绩效等需要排序的数据

官方命令介绍:Commands | Redis

  • 三种特殊数据类型

  • geospatial                【地理位置】

        Geo这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人

        从官网可以看到geo相关的命令有10条:

        Redis 从入门到精通_第2张图片

redis官网,geo相关命令详解:Commands | Redis

  1. GEOADD key longitude latitude member [longitude latitude member ...]     #增加指定的地理空间项目(经度、纬度、名称)到指定的关键。

  2. GEODIST key member1 member2 [m|km|ft|mi]     #返回由排序集表示的地理空间索引中两个成员之间的距离。

  3. GEOHASH key member [member ...]    #返回有效的Geohash字符串,表示地理空间索引的排序集值中一个或多个元素的位置

  4. GEOPOS key member [member ...]           #返回所有指定的地理空间索引成员的位置(经度、纬度),这些成员由键表示的排序集表示。

  5. GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC] [STORE key] [STOREDIST key]      #返回使用GEOADD填充地理空间信息的排序集的成员,这些成员位于用中心位置和到中心的最大距离(半径)指定的区域的边界内。
  6. GEORADIUSBYMEMBER   #该命令与GEORADIUS完全相似,唯一的区别是,它不接受经度和纬度值作为要查询的区域的中心,而是接受由排序集表示的地理空间索引中已经存在的成员的名称。

  • hyperloglog    【基数统计】        

什么是基数?

例如:有以下两个集合,分别是集合A{1,5,9,12,45,0,10,5,8,22} ,B{2,12,5,8,9,7,3,4}

那么A集合与B集合做基数,那么基数(就是集合中不重复的元素)= 6   (其中不同的元素是0,1,2,45,10,22)

优点:占用内存是固定的,2^64不同的元素基数,只需要费12KB内存!如果要从内存比较的话Hyperloglog首选

应用场景:

网页的UV(一个人访问一个网站多次,但是还是算作一个人!)

传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为统计保准。

这个方法如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id

0.81%错误率!统计UV任务,可以忽略不计的!

# 命令介绍
PFADD key element [element ...]        #创建一组元素

PFCOUNT key [key ...]                  #统计元素的基数数量

PFMERGE destkey sourcekey [sourcekey ...]    #合并多组元素  并集
  • bitmap            【位图】

SETBIT key offset value

GETBIT key offset

BITCOUNT key [start end]

  • 事务

        Redis单条命令是保证原子性的,但事务是不保证原子性的!

        Redis事务的本质:一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行!

        特点:一次性、顺序性、排他性。执行一系列的命令

        在Redis事务中,并没有直接被执行!只有发起执行的时候才会执行。

       redis的事务:

  • 开启事务(MULTI)
  • 命令入队()
  • 执行事务(EXEC)
  • 放弃事务(DISCARD)
  • 监视(WATCH key [key ...])相当于乐观锁的version
  • 放弃监视(UNWATCH)
  • Redis配置文件

        启动redis服务的时候就是通过配置文件来启动的。

进入到redis的配置文件redis.conf:

Redis 从入门到精通_第3张图片

1、配置文件unit单位 对大小写不敏感!

Redis 从入门到精通_第4张图片

2、可以包含多个配置文件

Redis 从入门到精通_第5张图片

3、加载一些so文件

Redis 从入门到精通_第6张图片

4、网络配置 ,这里可以绑定可以访问redis的ip地址

Redis 从入门到精通_第7张图片

5、保护模式,即是否收到保护。

7、设置端口号

Redis 从入门到精通_第8张图片

8、配置tcp连接

Redis 从入门到精通_第9张图片

9、配置连接超时时间

redis通用配置 GENERAL

是否开启进程守护 (默认情况下是 no ,就是没有开启进程守护)

 管理守护进程(使用默认的就行)

Redis 从入门到精通_第10张图片

 配置文件的pid文件位置(如果前面开启了以守护进程的形式在后台运行,那么这里就必须指定一个pid文件)

Redis 从入门到精通_第11张图片

 日志级别(4种日志级别,默认是 notice 级别)

Redis 从入门到精通_第12张图片

 生成的日志文件

 数据库数量(默认数据库数量是16个)

Redis 从入门到精通_第13张图片

 是否显示redis的logo        (默认是开启的)

Redis 从入门到精通_第14张图片

 快照配置 SNAPSHOTTING

 配置RDB持久化规则:Redis 从入门到精通_第15张图片

save 900 1         # 如果900秒内如果超过1个key被修改,那么就进行一次持久化
save 300 10        # 如果300秒内如果超过10个key被修改,那么就进行一次持久化
save 60 10000      # 如果60秒内如果超过10000个key被修改,那么就进行一次持久化

 如果持久化失败(持久化出错),是否还需要redis继续工作        (默认开启的)

Redis 从入门到精通_第16张图片

 是否对rdb持久化文件进行压缩(默认开启的)

Redis 从入门到精通_第17张图片

 是否检查校验rdb持久化文件(默认是开启的)

Redis 从入门到精通_第18张图片

 设置rdb持久化文件的文件名(默认的rdb持久化文件的文件名为dump.rdb)

 删除实例中复制使用的RDB文件,而不持久化 (默认是禁用的)

Redis 从入门到精通_第19张图片

 设置rdb持久化文件的保存目录

Redis 从入门到精通_第20张图片

 主从复制 REPLICATION

配置主机的host和port:

Redis 从入门到精通_第21张图片

 

当这两项配置好之后,将前面的 # 号去掉,然后重启,则该redis服务就会变成一个从机,并且会自动的去连接主机。

安全 SECURITY

 redis默认是没有密码的,如若要设置密码,则在下面加上 requirepass  "这里是你要设置的密码"。一般情况下我们不在配置文件中配置密码,而是在命令行里面设置。

root@dhy:/$ redis-cli -p 4832
127.0.0.1:4832> ping
PONG
127.0.0.1:4832> CONFIG GET requirepass        # 查看密码
1) "requirepass"
2) ""
127.0.0.1:4832> CONFIG SET requirepass "Great_monster"    # 设置密码
OK
127.0.0.1:4832> CONFIG GET requirepass     # 查看密码
1) "requirepass"
2) "Great_monster"
127.0.0.1:4832> exit        # 退出重新连接客户端,看密码是否生效

root@dhy:/$ redis-cli -p 4832    #重新进入客户端
127.0.0.1:4832> ping             # 测试连接
(error) NOAUTH Authentication required.        # 可以看到密码已经生效
127.0.0.1:4832> CONFIG GET requirepass
(error) NOAUTH Authentication required.
127.0.0.1:4832> AUTH Great_monster            # 验证密码
OK
127.0.0.1:4832> CONFIG GET requirepass        # 再次查看设置的密码,可以看到已经能正常访问了
1) "requirepass"
2) "Great_monster"
127.0.0.1:4832> ping
PONG
127.0.0.1:4832> 

Redis 从入门到精通_第22张图片

客户端配置 CLIENTS

 内存配置 MEMORY MANAGEMENT

 

Redis 从入门到精通_第23张图片

AOF持久化配置 APPEND ONLY MODE

 

 

  • Redis持久化

由于redis是内存数据库,如果不对数据进行持久化保存的话,那么存储的数据将会断电即失,及时在不断电的情况下数据也会有可能丢失,这是由于内存的存储方式和存储结构所决定的。所以想要保障数据不丢失,就要对其进行持久化。目前redis给出了两种持久化方案:RDB 和 AOF 两种持久化方案,默认使用的是RDB持久化方案。

  • RDB

简单的说RDB就是一种刷盘的数据持久化方案,通俗的讲就是快照。

触发机制

1、sava的规则满足的情况下,

2、执行flushAll命令

3、退出redis 

备份就自动生成一个dump.rdb

 如何利用rdb文件来恢复数据?

1、只需要将rdb文件放到其指定的目录下即可。

如何查看rdb文件的存放目录?

方法一:通过命令  config get dir  查看

 方法二:通过配置文件查看

Redis 从入门到精通_第24张图片

 rdb机制的优缺点

优点:

1、适合大规模的数据恢复

2、对数据的完整性要求不高

缺点:

1、需要一定的时间间隔进行持久化操作!在这个间隔期间突然出问题,可能导致最后一次或多次修改的数据丢失

2、fork子进程的时候,会占据一定的内存空间。

  • AOF

AOF这种数据持久化方案他就更像Linux系统中的history命令一样。他的持久化原理是记录我们执行的每一条写操作命令,以日志的形式进行追加。

Redis 从入门到精通_第25张图片

该机制是默认不开启的(文件的存储路径是和rdb文件的存储路径一致的,它们共用一个路径配置) 。

如果启用aof机制,当aof文件有错误的时候,redis是启动不起来的。

Redis 从入门到精通_第26张图片

 当我们再次连接的时候报错:Could not connect to Redis at 127.0.0.1:4832: Connection refused

 redis给我们提供了一个aof文件修复工具  redis-check-aof --fix 可用于修复aof文件(这里可能能修复成功,但不保证数据不会被修改或者丢失)

Redis 从入门到精通_第27张图片

 修复之后的aof文件:

Redis 从入门到精通_第28张图片

连接redis验证一下,看数据时候都在

$ redis-server /etc/redis/redis4832-telnet.conf 
$ redis-cli -p 4832
127.0.0.1:4832> ping
PONG
127.0.0.1:4832> SELECT 3
OK
127.0.0.1:4832[3]> keys *
1) "k2"
2) "k1"
127.0.0.1:4832[3]> get k1
"Great Monster"
127.0.0.1:4832[3]> get k2
"good boby"

可以看到数据也完全没问题。

这里看另外一种情况,当aof的出错形式不是以上情况时,看一下结果是怎么样的。

模拟篡改aof里面的值

Redis 从入门到精通_第29张图片

 这个时候连接redis发现还是报了 Could not connect to Redis at 127.0.0.1:4832: Connection refused 的错误,这个时候我们继续使用修复工具修复aof,当工具修复完成后我们可以看到aof文件里面的内容是这个样子的:

Redis 从入门到精通_第30张图片

 当我们连接redis后发现,数据全没了:

$ redis-server /etc/redis/redis4832-telnet.conf 
$ redis-cli -p 4832
127.0.0.1:4832> SELECT 3
OK
127.0.0.1:4832[3]> keys *
(empty array)
127.0.0.1:4832[3]> 

到了这里我们不难发现,这个aof修复工具的修复原理是样子的,他就是直接从最开始被改动的哪一行所对应记录的数据开始往后的内容都删掉,从而达到修复aof的目的。

aof优缺点

优点:

1、每一次修改都同步;文件的完整性会更好 。

2、每秒同步一次;可能会丢失一秒的数据。

3、从不同步;这时redis的效率是最高的。

缺点:

1、相对于数据文件来说,aof远远大于rdb,恢复的速度远远比rdb慢!

2、aof的运行效率也要比rdb慢(因为aof是io操作),所以redis默认情况下就是使用rdb持久化!

  • Redis 订阅/发布模式

第一个: 消息发送者;第二个:频道;第三个:消息订阅者!

Redis 从入门到精通_第31张图片

         Redis发布订阅(pub/sub)是一种消息通信通道:发送者(pub)发送消息,订阅者(sub)接收消息。Redis订阅端可以订阅任意数量的频道。

序号 命令 描述
1 PSUBSCRIBE pattern [pattern ...] 订阅一个或多个符合规定模式的频道
2 PUBLISH channel message 将消息发送到指定的频道
3 PUBSUB subcommand [argument [argument ...]] 查看订阅与发布系统状态
4 PUNSUBSCRIBE [pattern [pattern ...]] 退订所有给定模式的频道
5 SUBSCRIBE channel [channel ...] 订阅给定的一个或者多个频道信息
6 UNSUBSCRIBE [channel [channel ...]] 退订给定的频道

测试

 发布者

$ redis-cli -p 4832
127.0.0.1:4832> PUBLISH Great-Monster Hello,Great-Monster  # 发布者发布消息到频道
(integer) 1
127.0.0.1:4832> PUBLISH Great-Monster Hello,Redis!   # 发布者发布消息到频道
(integer) 1
127.0.0.1:4832> 

订阅者

127.0.0.1:4832> SUBSCRIBE Great-Monster            # 订阅一个频道 Great-Monster
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "Great-Monster"
3) (integer) 1
            # 等待读取推送的信息

1) "message"                  # 消息
2) "Great-Monster"            # 频道
3) "Hello,Great-Monster"      # 内容


1) "message"                  # 消息
2) "Great-Monster"            # 频道
3) "Hello,Redis!"             # 内容

原理

        通过 SUBSCRIBE 命令订阅某一频道后,redis-server 里面是维护了一个字典,字典的键就是一个一个的频道!而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。 SUBSCRIBE 命令的关键就是将客户端添加到给定 channel 的订阅链表中;通过 PUBLISH 命令向订阅者发送消息,redis-server会使用给定的频率作为键,在他维护的 channel 字典中查找记录了该订阅的频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。

         Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在 Redis 中,可以设定对某一个 key 值进行消息发布以及消息订阅,当一个 key 值上进行消息发布后,所有订阅它的客户端都会收到相应的消息,这一功能最明显的作用就是用作实时消息系统,比如普通的及时聊天,群聊等功能。

使用场景:

1、实时消息系统

2、实时聊天(频道当做聊天室,将消息回显即可)

3、订阅,关注系统

  • Redis主从复制

 概念

        主从复制是指将一台Redis服务器的数据复制到其他Redis服务器。前者称为主节点(master/leader),否则称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点,Master以写为主,Slave以度为主。

        默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或者没有从节点),但是一个节点只能有一个主节点。

主从复制的的主要作用:

       1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

        2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,实际上是一种服务冗余。

        3、负债均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供写服务(即写Redis数据时应用连接主节点,读取redis数据时应用连接从节点),分担服务器的负载;尤其是在写少读多的场景下,通过多个节点分担读负载,可以达大提高Redis服务器的并发量。

        4、高可用(集群)基石:除了上述作用以外,主从复制还是哨兵模式集群实施的基础,因此说主从复制是redis高可用的基础。

        一般来说,要将redis运用到项目中去,只是使用一台Redis是万万不能的(有宕机风险),主要原因有以下几点:

        1、从结构上来说,单个redis服务会发生但点故障,并且一台服务器需要处理所有请求负载,压力较大;

        2、从容量上来说,单台了redis服务器的内存容量是有限的。

主从复制,读写分离!80%的情况是在进行读操作!为减缓服务器压力,架构中至少是一主二从,或者一主多从的架构。

Redis 从入门到精通_第32张图片

 环境配置

        只用配置从库,不配置主库(默认情况下,每个节点都是一个主节点)!

127.0.0.1:4832> info replication        # 查看当前库的信息
# Replication
role:master                             # 角色 master
connected_slaves:0                      # 连接从机的数量 0
master_replid:1527f7aaa6f1cd1b15f1868eab777e653fd366fb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:4832> 

 做一个伪集群(一主二从)

 将redis的配置文件复制出三份出来:

 分别进入配置文件中,修改以下内容:

1、端口号

2、pidfile

3、logfile

4、.rdb 文件名

5、.aof的文件名

这里以redis79.conf配置文件为例:

1、将端口号改为6379(这里由于是复制redis配置文件过来的,这里端口号就可以不用改)

2、该pidfile:

 3、修改logfile

4、修改 .rbd 文件名

5、修改 .aof 文件名

         至此一个配置文件就初步修改好了。其他的80、81配置文件同理将端口号以及文件名修成成与之对应的即可!

启动服务

$ redis-server redis79.conf 
$ redis-server redis80.conf 
$ redis-server redis81.conf 
$

$ redis-cli -p 6379            # 启动端口号为6379的redis
127.0.0.1:6379> 


=========================================================================

$ redis-cli -p 6380            # 启动端口号为6380的redis
127.0.0.1:6380> 

=========================================================================

$ redis-cli -p 6381            # 启动端口号为6380的redis
127.0.0.1:6381> 

 使用 ps -ef|grep redis 查看进程,验证是否已开启相应的redis服务。

Redis 从入门到精通_第33张图片

 可以看到三个redis服务已经成功开启了。

一主二从配置(主机用79,从机用80、81)

 这里只用配置从机即可,主机不用配置。(俗称儿子找爸爸^_^)

序号 命令 解释
1 SLAVEOF host port 从机连接主机!host 主机地址,port 主机端口号

 配置从机80:

127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6380>

查看当前库信息:

Redis 从入门到精通_第34张图片

配置从机81

127.0.0.1:6381> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6381>

 查看当前库信息:

Redis 从入门到精通_第35张图片

这里再来看一下主节点79的状态(这时我们应该会看到79下面有80和81两台从机):

Redis 从入门到精通_第36张图片

 至此就已经完成了以79为主机,80和81是79的从机的“一主二从”的主从架构!

以上是使用命令来配置的主从复制(仅当前连接生效,宕机或重启断开连接后会失效),一般情况下回使用配置文件的新形势来配置(永久生效);配置文件配置从机的方法在上文介绍配置文件的时候有说道。

 测试

 注意:主机主要用作写(也可以读,但他有从机的话一般都不在主机上读数据,都在从机上读数据,主机上的数据会自动同步到从机上),从机只能读不能写!

主机79:

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set Great-Monster "hello Great-Monster"
OK
127.0.0.1:6379> keys *
1) "Great-Monster"
2) "k1"
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get Great-Monster
"hello Great-Monster"
127.0.0.1:6379> 

从机80:

127.0.0.1:6380> keys *
(empty array)
127.0.0.1:6380> get k1
"v1"
127.0.0.1:6380> get Great-Monster
"hello Great-Monster"
127.0.0.1:6380> keys *
1) "Great-Monster"
2) "k1"
127.0.0.1:6380> set k2 v2
(error) READONLY You can't write against a read only replica.    # 从机不能写
127.0.0.1:6380>

 可以看到在从机80上面是不能够写的,只能读!

 从机81:

127.0.0.1:6381> keys *
(empty array)
127.0.0.1:6381> keys *
1) "Great-Monster"
2) "k1"
127.0.0.1:6381> get k1
"v1"
127.0.0.1:6381> get Great-Monster
"hello Great-Monster"
127.0.0.1:6381> set k3 v3
(error) READONLY You can't write against a read only replica.  # 从机不能写
127.0.0.1:6381>

从机81也是一样!只能读,不能写!

Redis主从复制原理:

        Slave启动成功连接到 master 后会发出一个 sync 同步命令,当 master 接收到 sync 命令后会启动后台的存盘进程,同时收集到所有用于修改的数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。

        全量复制:Slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

        增量复制:Master持续将新的所有收集到的修改命令依次传给slave,完成同步,但是只要是重新连接master,一次完全同步(增量复制)将被自动执行!我们的数据一定可以在从机中看到!

当主机挂掉了,没有主节点,这个时候能不能选择一个老大出来呢?手动

 主节点断开:

 从节点想称为主节点:命令 SLAVEOF no one 让自己成为主节点!

 Redis 从入门到精通_第37张图片

 但是这样手动太麻烦,有没有自动的选择主机呢?(哨兵模式!)

  • Redis 哨兵机制

概念

        主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这需要人工干预,这样既费时又费力,还会造成一段时间内服务不可用。这不是我们想要,更多时候我们希望这个过程能自动实现。这时哨兵模式给我们提供了这样的解决方案,Redis从2.8开始正式提供了 Sentinel(哨兵)架构来解决这个问题。

        哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个单独的进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

Redis 从入门到精通_第38张图片

        以上是一个单机版的哨兵示意图,在正常情况下,我们哨兵也会搭建成一个集群的模式,让哨兵之间也进行相互的监控。

Redis 从入门到精通_第39张图片

         这里假设主服务器宕机,哨兵1(也可能是哨兵2或者哨兵3,反正就是会有一个哨兵会先监测)会先监测到这个结果,这时系统并不会进行failover(故障转移)过程,即当只有一个哨兵监测到主机宕机的时候,会主观的认为是主服务器不可用,这个过程称为主观下线。这时当其他的哨兵也监测到主服务器不可用的时候,并且监测到主机不可用的哨兵数量达到一定的数量时(即,大多数哨兵都监测到了主服务器不可用,这时系统就会认为主服务器可能真的宕机了),那么这个时候哨兵之间就会进行一次投票选举,从剩下的可用的redis服务其中选举出一个新的主服务器,投票的结果会由一个哨兵发起进行failover(故障转移)操作。切换成功后会通过发布/订阅的模式让各个哨兵把自己监测的被选举的从服务器切换成主服务器,这个过程称为客观下线

测试

        当前的测试环境是一主二从的环境!

        1、配置哨兵的配置文件

                (若redis配置文件目录里面没有哨兵配置的话就自己建一个配置文件,以下是一些主要的配置内容)在 redis 服务器的配置文件目录下新建一个 sentinel.conf ,并在配置文件中加入相关配置:

# 配置哨兵的端口号
# port 26397

# sentinel monitor    
# sentinel 哨兵   monitor 监测   需要监测集群名字,名字可以随便去  
#  服务器地址   端口号   表示当有 votes 个哨兵认为主节点
# 失效时,主节点才会失效,从而触发选举(这里的votes理应小于或等于哨兵的数量)。
# sentinel monitor mymaster 127.0.0.1 6379 1

# sentinel down-after-milliseconds  
# 指定Sentinel判断实例进入主观下线所需的时间长度(毫秒)
# sentinel down-after-milliseconds mymaster 10000

# sentinel auth-pass  
# 设置连接master和slave时的密码,master和slave的密码应该设置相同
# sentinel auth-pass mymaster pw‘’

# 故障转移超时时间
# sentinel failover-timeout mymaster 180000


# 是否设置脚本从配置
# sentinel deny-scripts-reconfig yes

        当redis配置文件目录下有哨兵的配置文件时,直接修改配置文件即可!

Redis 从入门到精通_第40张图片

可以看到,redis配置文件目录下是有哨兵的默认配置文件的! 打开哨兵默认配置文件:

1、基础配置

Redis 从入门到精通_第41张图片

Redis 从入门到精通_第42张图片

 Redis 从入门到精通_第43张图片

 2、核心配置

Redis 从入门到精通_第44张图片

Redis 从入门到精通_第45张图片

Redis 从入门到精通_第46张图片

Redis 从入门到精通_第47张图片

 以上就是哨兵的配置文件的基本内容,其余的脚本执行和安全类的配置这里就不在列举!

        这里就使用默认的配置文件进行修改即可,我们有一台主机 6379 和两台从机 6380 和 6381 ,我们就配置3个哨兵(一般来说,哨兵的数量一般为奇数,如1、3、5...)。

1、复制一份sentinel.conf文件到我们统一的config目录下,原生的redis目录下就存在sentinel.conf
cp sentinel.conf config/sentinel-26379.conf

2、将相关配置修改为如下值:
  port 26379
  daemonize yes
  pidfile "/var/run/sentinel/redis-sentinel_26379.pid"
  logfile "/var/log/redis/redis-sentinel_26379.log"
  dir "/var/lib/redis"          # 这里可以不用改
  sentinel monitor mymaster 127.0.0.1 6380 2         # mymaster这个名字随便取,客户端访问时会用到

3、重复以上步骤再配置两个sentinel,端口26380和26381,注意上述配置文件里的对应数字都要修改

 启动哨兵:

$ redis-sentinel sentinel_26379.conf 
$ redis-sentinel sentinel_26380.conf 
$ redis-sentinel sentinel_26381.conf

检查是否正常启动:         Redis 从入门到精通_第48张图片

 当出现以上红框里面的信息,说明哨兵已经正常启动了!

 验证哨兵模式是否能正常选举主机:

        当前可以看到主机是6379、从机是6380和6381,并且都正常工作了!

Redis 从入门到精通_第49张图片

再看一下从机的状态:

Redis 从入门到精通_第50张图片

 Redis 从入门到精通_第51张图片

        可以看到从机6380和6381都已经正常工作了,这个时候我们仍未的让主机6379宕机,看一下哨兵是否能自动的选出主机。

        1、将6379主机shutdown掉,模拟主机宕机的情况:

        

Redis 从入门到精通_第52张图片

         可以看到主机6379已经宕机,这时来看一下剩下的两个从机的状态,看一下哨兵是否能自动的从两个从机中选举出主机。

Redis 从入门到精通_第53张图片

         我们再去redis客户端里面看一看,验证一下哨兵模式输出的结果是否正确:

        先看81,看一下是否已经成为主机:

        Redis 从入门到精通_第54张图片

        可以看到81服务器已经成为主机,80服务器成为了81服务器的从机,目前至少说明哨兵的输出日志在81这台服务器上是正确的!然后再来检查一下80服务器:

        Redis 从入门到精通_第55张图片

        可以看到80服务就是81服务器的从机,到此可发现哨兵已经完成了它的使命。即,当主机宕机时,哨兵会从剩下的从机当中选出一个来顶替挂掉的主机。

        思考:如果这个时候79服务器恢复了,那79服务器是恢复成主机还是变成81的从机?

        我们可以将79服务启动一下,看看79服务器状态是什么样的。启动79服务器后可以看到哨兵的输出日志里面显示79服务器已经变成了81服务器的从机了!     

         这时我们到81服务器里面验证一下79服务器是否真的成为了81的从机,同时也到79服务器里面也验证一下它的主机是否是81服务器。

        首先是81服务器:

        Redis 从入门到精通_第56张图片

        然后是79服务器:

        Redis 从入门到精通_第57张图片

         到这里,发现79服务器这时的的确确已经成为了81的从机了!

        到此我的可以的出结论:在哨兵模式下,当主机宕机,哨兵重新选出主机之后又重新上线时,它只能变成从机加入到新选出的主机!

        哨兵的优缺点

优点:

         1、哨兵集群,基于主从复制模式,所有的主从配置优点在哨兵模式上也有。

         2、主从可以切换,故障可以转移,系统的可用性会更好。

         3、哨兵模式就是主从模式的升级,手动到自动,更加健壮。

缺点:

         1、Redis在在线扩容方面相对较为困难,集群容量一旦达到上限,扩容就十分麻烦!

         2、实现哨兵模式相对较为麻烦,哨兵的配置较为繁琐。

  •  Redis缓存模型及分析

 Redis 从入门到精通_第58张图片

        以上是web端向服务器请求数据的基本流程!首先查缓存,若缓存中有web端想要的数据就直接将数据响应给web端;倘若发现缓存中没有web端想要的数据,那么就会直接去查询数据库,从数据库中获取数据,然后数据库键web端想要的数据响应给web端。到此这个缓存模型的执行流程结束!

        该模型的问题:

               这个模型想要正常高效的按照我们的预期执行是有三个重要前提条件的!

        第一个前提条件:默认web端请求的绝大多数数据在redis中都有缓存,即web端绝大多数请求都能在Redis缓存中拿到想要的数据!

        第二个前提条件:数据库中是有web端所需要的所有数据,即数据库是为redis缓存兜底的,就算redis中没有web端请求的数据时,数据库中是一定有web端想要的数据的!

       当这两个重要前提条件中任意一个出现问题时,该模型都会远远的达不到我们的预期。即,当这两个重要前提条件不满足时就会引发相对应的缓存问题:缓存雪崩缓存穿透缓存击穿

  • 缓存穿透及解决方案

什么是缓存穿透?

        缓存穿透,什么是穿透?穿透的又是什么?穿透和击穿又是什么关系?从字面上来理解缓存穿透就是穿透了缓存,但是这里远远没有这么简单,在穿透缓存的同时还穿透了数据库,即这里的穿透指的不仅仅是缓存,而是整个缓存模型!这里我们很容易搞不清击穿和穿透这两个概念,从字面上来说击穿和穿透都是描述破坏程度的这么一个词,他们的区别只是程度不同而已。比如:一个普通人拿弓箭去射击一个浑身穿着铠甲的人,并且弓箭射中了这个穿着铠甲的人,箭支留在了穿着铠甲的人身上,我们对这支箭的破坏程度就会说这支箭击穿了铠甲(这时可能就只是刚刚击穿铠甲而没有对铠甲里面的人造成伤害,或者是对铠甲里面的人造成了伤害,当造成的伤害也不是很大),在射箭人的眼中箭只是命中了目标。但是当射箭的人换成了一个武功盖世的高人来射击的话,结果是箭支直接透过铠甲将人射了个对穿,穿着铠甲的人当场身死!这时我们再来看这支箭的威力就是直接穿透了身穿铠甲的这个人,可谓是老母牛坐飞机,牛壁上天了!看到这里应该能理解击穿和穿透的区别了,这里面的箭就是web端的数据请求,铠甲就是相当于redis缓存,数据库就是那个穿铠甲的人了。所以说缓存穿透是比缓存击穿更严重的问题,缓存击穿只是击穿了redis缓存并没有击穿数据库,而缓存穿透,它不仅击穿了redis缓存,还击穿了数据库!这里在解答一下穿透什么?这里穿透指的是web端在redis缓存中没有拿到数据的同时在在去查数据库都也没有拿到相应的数据;即,在缓存中没拿到数据,在数据库中也没有拿到数据,这就是缓存穿透!这里就对应了前面缓存模型中提到的第二个前提条件是不满足或者是出现问题导致的。

解决方案     

         前面说明白了什么是缓存穿透和造成缓存穿透的原因后,这里解决缓存穿透问题的思路就很明了了。说白了造成缓存穿透的根本原因就是不满足前面说的第二个前提条件,那我们让它满足这个前提条件不就完美解决了嘛!这里再来看一下第二个前提条件中主要说了什么?主要就是说web端请求的数据在必须都要在数据中,反过来说就是不论web端请求什么样的数据,都能在数据库中拿到。这时造成缓存穿透的根本元凶就渐渐浮出水面了,其原因就是web端请求的数据在数据库中没有造成的!

        到这里我们解决缓存穿透问题主要就分成了三个思路:

        第一,在数据库中添加可预期的数据(该方法可在一定程度上解决)!

        第二,对异常请求数据做统一的异常处理(该方法基本上能解决绝大多数穿透问题,推荐使用该方法)!

        第三,单独隔离出一个防火墙系统,对请求数据做实时监测(该方法能在最大程度上解决穿透问题,但实现成本较大)!

  • 缓存击穿及解决方案

什么是缓存击穿?

       上面我们有提到缓存击穿的一部分概念,就是在redis缓存中查不到数据,从而导致请求打到数据的情况。但是看到这里我们还是不明白直接打到数据库的根本原因,所以我们还是要具体来分析一下导致这种情况的具体原因。当我们来设想一下这样的一种情况:我们是做有一个非常牛逼的电商网站(牛逼到什么程度呢?只要你能想到的东西在我们的电商网站上都能买的到,诶!就是这么牛逼!)。有一天,有一个几千万粉丝量级的非常漂亮的美女大主播在直播间突然向她的水友们推销到道某某牌子的擀面杖非常好用,让他的水友们都去买。这时这个美女主播的水友们就都去我们的电商网站购买,同时这几千万水友们有向身边的亲朋好友推销,导致某某牌的擀面杖突然大火,一下子有上亿的顾客都到我们的电商网站来搜索并购买这个某某牌的擀面杖。这时我们的电商网站中的这个“某某牌擀面杖”这个字段就一下子要承受上亿的查询请求。这时就出现以下情况时我们的电商网站就会瞬间被这些请求淦崩溃:第一、我们的redis缓存中有这个“某某牌擀面杖”的缓存,但是没一会儿就因为过期时间到了;第二、我们的redis缓存中从始至终就没有缓存到redis中过(因为我们没有料到某某牌擀面杖竟然会突然这么火)。以上这两点原因,不管是因为那一个原因导致数据库宕机,我们都称这一现象为缓存击穿!

解决方案

        第一、不设置过期时间(下下之策)

        第二、分布式锁,将并发压力去给到分布式锁(对分布式锁的压力较大)

  • 缓存雪崩及解决方案

什么是缓存雪崩?

        如何理解缓存雪崩:发生雪崩时没有一片雪花是无辜的……

  Redis 从入门到精通_第59张图片

         缓存击穿是指在某一时刻,redis中大量的key失效,然而就在这一时刻前端都进来大量的请求,这个时候在redis中时拿不到数据(因为redis中的数据已经失效查不到数据)!这些请求就会直接去查询数据库,数据库的压力就会剧增,就可能导致服务宕机,redis在这个时候并没有实现它应有的价值(即帮助数据库减轻压力)!这时我们就称这种现象为缓存击穿。

         缓存雪崩和缓存击穿其实很像,很容易弄混淆,因为这两个问题的出现都是因为web端在Redis缓存中拿不到数据,从而需要去数据库中获取,但是造成这两个问题的具体原因又不一样,所以很容易混淆这两个这两个概念。缓存雪崩是指短时间内大量的热点数据几乎同时失效(大量热点key几乎同时过期)造成的;而缓存击穿是某一个数据非常火热,web端有大量的请求都是查询该热点数据,然而在某一时刻该热点数据却在缓存失效了,从而导致大量的请求打到数据库上。

解决方案

         可以看到缓存击穿是由于大量缓存数据失效造成,所以在解决方案也比较简单直接,那就是确保缓存数据不会大面积失效就解决了嘛!针对这个问题主要的解决办法就是在key的过期时间的基础上增加一个随机的时间片来解决短时间大面积缓存失效。

  • 基础API与jedis详解

什么是jedis ?

         jedis是官方推荐的Java连接工具!使用java操作Redis的中间件!

测试

 导入依赖:


        
            com.great_monster
            redis01-jedis
            1.0-SNAPSHOT
        


        
            com.alibaba
            fastjson
            1.2.7
        

编码测试 :

  • 编码测试 
package com.great_monster;

import redis.clients.jedis.Jedis;

public class TestPing {
    public static void main(String[] args) {
        // 1、new Jedis 对象即可
        Jedis jedis = new Jedis("192.168.1.129",4832);
        //jedis 所有的方法就是redis的命令
        System.out.println(jedis.ping());

    }
}

jedis事务:

import redis.clients.jedis.Transaction;

public class testTx {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.1.129",4832);  // 连接redis

        jedis.flushDB();

        Transaction multi = jedis.multi();      //开启事务

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello","world");
        jsonObject.put("name","great_monster");
        String result = jsonObject.toJSONString();

        try {
            multi.set("user1",result);
            multi.set("user2",result);
            multi.exec();           //执行事务
        }catch (Exception e){
            multi.discard();        //当出现异常时放弃事务,回滚数据
            e.printStackTrace();
        }finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            jedis.close();          //关闭连接
        }
    }
}

结果:

Redis 从入门到精通_第60张图片

  • SpringBoot集成Redis操作

说明:在Springboard 2.x之后,原来使用的jedis被替换成了lettuce。

jedis:采用直连,多个线程操作的话,是不安全的,如果想要避免这一问题,需要使用jedis pool 连接池!更像BIO(阻塞的)

lettuce:采用netty,实例可以在多个线程之间共享,不存在线程不安全的情况!更像NIO(异步的)

编码:

导入redis依赖:


        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            org.springframework.boot
            spring-boot-starter-web
        

SpringBoot 所有的配置类,都有有一个自动配置类,自动配置类会绑定一个 properties 配置文件:

Redis 从入门到精通_第61张图片

在springboot的自动配置里面找到redis的配置类:

 配置类:RedisAutoConfiguration

Redis 从入门到精通_第62张图片

 绑定的配置文件:RedisProperties

Redis 从入门到精通_第63张图片

 配置类给我们注册了两个redis模板:RedisTemplate 、StringRedisTemplate

Redis 从入门到精通_第64张图片

源码分析:

@Bean
	@ConditionalOnMissingBean(name = "redisTemplate") //这里我们可以自己定义一个自己的redisTemplate模板来替换默认的!
	public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
//默认的RedisTemplate 没有过多的设置,然而 redis 对象都是需要序列化的!
//两个泛型都是 Object,Object 的类型,我们后面需要强制类型转换
		RedisTemplate template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean  // 由于String 类型是Redis中最常用的类型,所以就单独注册了一个Bean!
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

配置redis的配置类:

查看配置类绑定的配置文件以及可配置的内容:

Redis 从入门到精通_第65张图片

 如图所示,找到配置类绑定的配置文件,该类的属性即是我们可以配置的内容。

编写redis配置文件:

# 配置 redis
spring.redis.host=192.168.1.129
spring.redis.port= 4832

测试连接:

RedisTemplate类简单介绍:

Redis 从入门到精通_第66张图片

测试编码:

@SpringBootTest
class Redis02SpringbootApplicationTests {

    // 注入RedisTemplate
    @Autowired
    RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        // redisTemplate  操作不同的数据类型
        // opsForValue 操作字符串 类似String
        // opsForList  操作List
        // opsForSet   操作Set
        // opsForZSet  操作ZSet
        // opsForHash  操作Hash
        // opsForGeo   操作Geo
        // opsForHyperLogLog  操作HyperLogLog
        // opsForCluster  操作位图

        // 除了基本的操作,我们常用的方法都可以直接通过 redisTemplate 操作,比如事务和CRUD

        // 获取redis的连接对象
        //        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        //        connection.flushDb();
        //        connection.flushAll();

        redisTemplate.opsForValue().set("mykey","great monster universe invincible");
        System.out.println(redisTemplate.opsForValue().get("mykey"));

        redisTemplate.opsForValue().set("mykey1","大大怪将军宇宙无敌");
        System.out.println(redisTemplate.opsForValue().get("mykey1"));

    }

}

运行结果:

问题: 为什么redis存入中文时,在java中是正常的,在redis客户端里面出现了乱码?

这是由于RedisTemplate默认的序列化方式是使用JDK序列化,从而使字符发生转义所产生的乱码问题。这里可以看一下配置类的源码,在源码当中可以找到答案:

Redis 从入门到精通_第67张图片

但我们点进RedisTemplate类时可以看到它的序列化方式:Redis 从入门到精通_第68张图片

Redis 从入门到精通_第69张图片

    @Test
    public void test() throws JsonProcessingException {
        // 使用json来传递对象(即,对对象进行序列化)
        User user = new User("Great Monster", 3);
        String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",jsonUser);
        System.out.println(redisTemplate.opsForValue().get("user"));

        //直接传递(不对对象进行序列化)
        redisTemplate.opsForValue().set("user1",user);
        System.out.println(redisTemplate.opsForValue().get("user1"));
    }

 运行结果:

Redis 从入门到精通_第70张图片

这里不对对象进行序列就会产生序列化报错,所以所有的对象都需要序列化。 实体类序列化只需要实现接口 Serializable 即可。

@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
// 序列化实体类,只需要实现接口 Serializable 即可
public class User implements Serializable {

    private String name;
    private int age;
}

 运行结果:

如何解决序列化产生乱码的问题?

 这里我们通常一般使用Json来序列化,基于以上情况,我们可以自己定义一个RedisTemplate来替换掉默认的,从而解决默认RedisTemplate所带来的一系列问题。编写自己的RedisTemplate类:

@Configuration
public class RedisConfig {

    // 编写自己的 redisTemplate

    @Bean
    @SuppressWarnings("all")
    // 将泛型更改为 
    public RedisTemplate redisTemplate(RedisConnectionFactory factory)
            throws UnknownHostException {
        RedisTemplate template = new RedisTemplate();
        // 连接工厂
        template.setConnectionFactory(factory);

        //Jackson序列化配置
        //使用Jackson来序列化,故这里先new一个Jackson的对象
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 使用ObjecMapper来进行转义
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 转义完就可以使用了
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key 采用 String 的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash 的 key 也采用 String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value 序列化方式采用Jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 的 value 序列化也采用Jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 把所有的的 Properties 都 set 进去
        template.afterPropertiesSet();

        return template;
    }

}

补充:通过源码可以看到其方法的能够支持的序列化方式

Redis 从入门到精通_第71张图片

 再一次运行测试类:

java端的运行结果:

redis客户端:

 

但是但我们get的时候发现还是会出现乱码:

Redis 从入门到精通_第72张图片

 这个结果跟我们的java端出入还是很大的,这时该如何解决呢?

在登录redis客户端的时候在命令后面加上  --raw   ,然后进入到客户端再 get 就不会再出现乱码了

 Redis 从入门到精通_第73张图片

其中的user中出现的转义符 因为对象另外使用了json转换,故出现了转义符,这个不属于乱码的范畴。至此乱码的问题解决了。

自定义封装RedisTemplate:

package com.grea_monster.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    public Set sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }


}
 
  

 测试自己封装的工具:

    @Autowired
    private RedisUtil redisUtil;

    @Test
    public void test1(){
        redisUtil.set("name","Great Monster");
        System.out.println(redisUtil.get("name"));
    }

 运行结果:

Redis 从入门到精通_第74张图片

  • Redis 的实战分析

        持续更新中……

  • 锁(Redission)

        持续更新中……

你可能感兴趣的:(Java,Redis,redis,数据库,缓存)