1、Redis基本知识
简介
Redis是一个支持网络、基于内存、可选持久性的NoSql数据库,目前在很多的系统中都使用了Redis,尤其是在实现缓存功能的时候应用的尤其广泛(缓存功能也是很多人对Redis的认识),那么Redis到底有哪些优点和缺点,为什么会被广泛应用呢?
Redis的优点
Redis的第一个优点就是速度快,Redis使用C语言实现,基于内存,数据的读写效率非常的高,这也是为什么很多系统的缓存功能使用Redis来实现,但是需要明确的是Redis是一个数据库,缓存只是它的一项应用而已。
Redis的第二个优点是单线程模型,所谓单线程模型就是每一个请求都会有一个全新的线程来进行处理,这一点类似于Struts2,每一个请求都会有一个新的线程来进行处理。这样做的好处就是避免了线程频繁切换带来的系统开销,同时也避免了让人头疼的多线程问题。
Redis的第三个优点就是使用了非阻塞I/O (NIO),不在网络上浪费时间,进一步提高了效率。
Redis的第四个优点就是支持多种的数据类型,并且每一种数据类型都提供了丰富的操作命令,适用于很多特殊的场景,并且支持自定义命令创建个性化的操作命令。
2、redis持久化机制
redis服务器宕机,内存数据是会丢失了,为了保证数据不丢失需要对数据做备份,所备份就是持久化,Redis的持久化即将内存中的数据同步到硬盘,主要包括两种方式RDB、AOF。
RDB持久化机制,(默认使用):做当前内存数据的全本快照,
将内存中的数据以快照的方式写入到二进制文件dump.rdb,
在redis.conf中可以设置发起快照保存的条件。在指定的时间内如果有超过指定数量的key被修改,则会发起快照保存。
这种方式在数据的实时性上不高,在突然断电的情况下,可能会出现部分数据的丢失,即最后一次快照之后在内存中发生修改的数据。
简单来说:RDB就是将redis上的所有数据做个备份,存储的是二进制的数据。
AOF持久化机制,(默认是关闭):
AOF是将Redis内存数据库中更改的数据都记录到指定的文件appendonly.aof。在redis.conf中可以进行写磁盘的相关设置。
在突然断电的情况下,由于在appendonly.aof中保存了最后一次写磁盘之后redis内存发生数据修改的指令,所以在这个Redis重启后,基本不会发生数据丢失,比RDB具有更好的数据安全性。
appendfsync always 接收到更改数据的命令,立即将其记录到appendonly.aof中,能保证数据持久化,数据完全不丢失,但效率相对最低。
appendfsync everysec 每秒钟将redis内存数据修改的命令记录到appendonly.aof中,在性能和持久化上做了折中。因频繁执行磁盘操作,在仅存在单个Master执行写操作时,效率可能存在问题。但在多个Master执行写操作的Redis集群中,效率会提升。
appendfsync no 依赖于操作系统,因不会频繁执行磁盘操作而性能最好,但redis内存数据修改持久化没有保证,无法保证数据可靠性
如何开启AOF持久化:
将redis.conf文件中 appendonly 改成 yes ,自动创建appendonly.aof,该文件存储的客户端执行过增删改操作的命令
3、 Redis的数据结构
redis数据库存储数据使用的key-value,键值对方式存储
key是string类型 value的数据结构支持5个string、set、sorted_set、list、hash
4、Redis 的常用命令
String(可以存数字)
可以实现原子性的自增(数据安全)
这是最基本的类型了,没啥可说的,就是普通的set和get,做简单的k-v缓存
hash
这个是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在redis里,然后每次读写缓存的时候,可以就操作hash里的某个字段。
key=150
value={
“id”: 150,
“name”: “zhangsan”,
“age”: 20
}
hash类的数据结构,主要是用来存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值
value={
“id”: 150,
“name”: “zhangsan”,
“age”: 21
}
list
有序列表,这个是可以玩儿出很多花样的
微博,某个大v的粉丝,就可以以list的格式放在redis里去缓存
key=某大v
value=[zhangsan, lisi, wangwu]
key=书名
value=[评论1, 评论2, 评论3]
比如可以通过list存储一些列表型的数据结构,类似粉丝列表了、文章的评论列表了之类的东西
比如可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询,这个很棒的一个功能,基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走
比如可以搞个简单的消息队列,从list头怼进去,从list尾巴那里弄出来
set
无序集合,自动去重
直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于jvm内存里的HashSet进行去重,但是如果你的某个系统部署在多台机器上呢?
得基于redis进行全局的set去重
可以基于set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧
把两个大v的粉丝都放在两个set中,对两个set做交集
sorted set 跳表
排序的set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序,这个可以玩儿很多的花样,最大的特点是有个分数可以自定义排序规则
比如说你要是想根据时间对数据排序,那么可以写入进去的时候用某个时间作为分数,人家自动给你按照时间排序
排行榜:将每个用户以及其对应的什么分数写入进去,zadd board score username,接着zrevrange board 0 99,就可以获取排名前100的用户;zrank board username,可以看到用户在排行榜里的排名
总结:优先掌握 String 即可
5、数据失效时间
场景:
手机验证码登录 手机验证码注册
验证码后台生成 Redis
- 集中存储
- 可以设置过期时间
验证码
key = 手机号
value = 验证码
设置3分钟过期
Redis中可以设置数据的存活时间
命令
expire key 存活时间的秒
ttl key 查看key对应的数据的存活时间.
pexipre key 存活时间的毫秒
pttl key 查看key对应的数据的存活时间,毫秒单位
expire key 存活时间的秒
失效的原理
-
定期随机删除+惰性删除
key 1 1分钟 2 1分钟 3 1分钟 4 1分钟 5 6 redis 每过100ms 随机抽取一定数量的设置了失效时间的key 将过期的删除 有些key过期了 每次都没有随机到 就一直删不掉 怎么办? 惰性删除 get key 的时候 先判断 key是否过期 如果过期 返回数据为空
定期随机删除 例如100ms
查询的时候 先检查key
-
内存淘汰机制
如果redis的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略:
redis 10个key,现在已经满了,redis需要删除掉5个key
1个key,最近1分钟被查询了100次
1个key,最近10分钟被查询了50次
1个key,最近1个小时被查询了1次
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
LRU算法 扩展视野
6、Redis的基本应用!!!
二级缓存
-
为什么要做缓存
-
缓存的选择(二级缓存)
-
二级缓存的基本概念
- 是什么
- 如何开启
-
二级缓存的缺点
-
使用Redis集成二级缓存的步骤
- 实现cache接口
缓存的作用
- 数据从内存获取,提升数据获取速度.
- 减轻了数据库读操作的访问压力(数据基本不变)
MyBatis二级缓存机制(开启)
机制:
-
Java本地缓存空间.(jar)
-
mybaits事务提交后操作缓存
-
Mybatis根据Mapper文件的namespace划分多个缓存空间.
-
mybatis会将查询语句执行结果,缓存在 sql所在mapper文件对应的namespace对应的缓存空间中.
会将执行的查询sql(对sql处理后产生的对象)作为key.
-
MyBatis执行DML,在事务提交之后,默认清空当前sql所在的mapper文件对应的namespace对应的缓存空间中
A Mapper User表 脏读
B Mapper User表 删除 缓存清空
缓存空间融合
- 所有关于User表的操作都写在一个Mapper中
- Mapper配置 融合缓存空间(基本没有人使用)
- 第三方的缓存空间
- ehcache
- Redis
MyBatis缓存实现原理(源码)
org.apache.ibatis.cache.impl.PerpetualCache.class
-
根据namepace划分缓存空间(id)
-
MyBatis二级缓存本质是一个Map结构
key :和执行的sql先关
value:查询结果相关
-
存放数据的功能: select语句(key)----查询结果(value)
-
获得数据的功能: 根据key
-
清空缓存的功能: clear
-
MyBatis管理每个缓存,使用Map管理 key:id(namespace) value:PerpetualCache
MyBatis缓存的问题?(缓存数据量不能太多)
- mybatis缓存在tomcat的jvm内部分配的缓存空间.
- 缓存数据过多,挤占java运行期间需要的内存.
解决办法:
将Mybatis的二级缓存空间转移到Redis数据库中
Mybatis二级缓存空间划分
Redis缓存空间的划分设计
核心:
- 每个缓存空间是一个map
- 每个缓存空间对应一个namespace.(管理多个cache空间)
方案:
- 将mybatis的namespace作为redis的key
- 将key对应的value作为hash数据结构使用.(替换PerpetualCache)
自定义Redis缓存实现
自定义缓存实现类
-
自定义MyBatis二级缓存
- 自定缓存类实现Cache接口
- 导入redis操作相关的工具(jar,JedisUtil,jedis.properties)
- 必须具备如下功能:
-
根据namepace划分缓存空间(id)
-
MyBatis二级缓存本质是一个Map结构
key :和执行的sql先关
value:查询结果相关
-
存放数据的功能: select语句(key)----查询结果(value)
-
获得数据的功能: 根据key
-
清空缓存的功能: clear
使用自定义的缓存
Session共享
为何要实现session共享?
nginx负载均衡,希望兼顾权重的按照硬件性能分配访问压力的优势,又想保证多个tomcat使用同一个session应该怎么解决?
- ip_hash
- session复制
- Redis
解决方案
使用redis管理负载均衡中多个tomcat的session.(Redis共享session)
如何实现Redis管理Session
配置步骤
- tomcat使用redis管理session的jar
将jar拷贝tomcat中lib
- 配置tomcat的session管理方式为RedisSessionManager
1. tomcat 配置文件context.xml
<!--注册session管理工具-->
<Manager className="session管理工具全类名"
host="redis的ip地址"
port="端口"
maxInactiveInterval="session存活时间 秒 1800" 秒
/>
<!--将session管理工具使用在tomcat操作过程中-->
<Valve className="RedisSessionHandlerValve在tomcat中使用session管理工具"/>
重启两个tomcat
缓存问题
-
缓存穿透
-
缓存雪崩
User 根据主键查询 key 1 2 3 4 5 6 7 8 9 1W -id 缓存中都没有 2000个查询 每秒 可以认为是安全的
缓存穿透
缓存击穿 大量不存在的key攻击
只需要极少的空间就可以判断一个元素是不是在一个集合之内,这正好是我们所需要的场景啊:判断key是否存在
解决方案
-
空值缓存 (非恶意攻击)
key value -1 null
-
布隆过滤器
可以判断key是否在数据库中存在
缺点:可能会判断出错 概率不高 但是会
缓存雪崩
微博
key = 鹿晗微博1 value = 相关信息 评论 追评 点赞 等
key = 鹿晗微博2 value = 相关信息 评论 追评 点赞 等
key = 鹿晗微博3 value = 相关信息 评论 追评 点赞 等
key = 鹿晗微博4 value = 相关信息 评论 追评 点赞 等
key = 鹿晗微博5 value = 相关信息 评论 追评 点赞 等
key = 吃瓜群众1
key = 吃瓜群众2
key = 吃瓜群众3
key = 吃瓜群众4
key = 吃瓜群众5
上千万key 这些key一定会设置失效 失效时间设置的不合理 同一时间大量key过期了(500W) 如果发生在平时 无所谓
不巧的是 上热搜了 突然间 大量的用户来访问 相关的信息
大量key同一时间失效 将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义
大量流量 且数据失效 导致不存在的数据每次请求都要到存储层去查询 一模一样的SQL 数据库崩溃
- 合理的设置过期时间 单体架构
- 分布式锁
- 多级缓存
分布式的锁,谁获得了这把锁,谁就可以访问数据库
大型项目中
如果说用户查不到数据 降级服务
- 先等着 两三秒
- 能直接 返回固定数据
- 等等
7、Redis 分布式锁
什么是分布式锁
一种逻辑处理
Redis 分布式锁
分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。
// 这里的冒号:就是一个普通的字符,没特别含义,它可以是任意其它字符,不要误解
> setnx lock:codehole true
OK
... do something critical ...
> del lock:codehole
(integer) 1
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
于是我们在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。
> setnx lock:codehole true
OK
> expire lock:codehole 5
... do something critical ...
> del lock:codehole
(integer) 1
但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。
这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。也许你会想到用 Redis 事务来解决。但是这里不行,因为 expire 是依赖于 setnx 的执行结果的,如果 setnx 没抢到锁,expire 是不应该执行的。事务里没有 if-else 分支逻辑,事务的特点是一口气执行,要么全部执行要么一个都不执行。
为了解决这个疑难,Redis 开源社区涌现了一堆分布式锁的 library,专门用来解决这个问题。实现方法极为复杂,小白用户一般要费很大的精力才可以搞懂。如果你需要使用分布式锁,意味着你不能仅仅使用 Jedis 或者 redis-py 就行了,还得引入分布式锁的 library。
为了治理这个乱象,Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁的乱象。从此以后所有的第三方分布式锁 library 可以休息了。
> set lock:codehole true ex 5 nx
OK
... do something critical ...
> del lock:codehole
上面这个指令就是 setnx 和 expire 组合在一起的原子指令,它就是分布式锁的奥义所在。