Redis缓存学习笔记
技术分类:
1、解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN
2、解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、Mybatis
3、解决性能的问题:NoSQL、Java 线程、Hadoop、Nginx、MQ、ElasticSearch
关系型数据库与非关系型数据库:
1、关系型数据库:
指采用了二维表格模型来组织数据的数据库
(1)、优点:
1)、易于维护(格式一致)
2)、sql通用,适用简易,而且也支持复杂操作(一个表以及多个表之间非常复杂的查询)
(2)、缺点:
1)、读写性能比较差,如果遇到大数据的高效率读写
2)、结构固定,导致灵活性很低
2、非关系型数据库:
分布式且一般不保证遵循ACID(原子性、一致性、持久性、隔离性)原则
(1)、优点:
1)、根据自已添加的字段获取用户不同信息。不像关系型数据库要多表关联查询
2)、扩展性高,适用SNS中,一些软件上功能系统的提升。因为其结构问题,严格上不是一种数据库,是一种数据结构化存储方法的集合
3)、速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘
4)、成本低:nosql数据库部署简单,基本都是开源软件
(2)、缺点:
1)、数据结构相对复杂,对于需要进行较复杂查询的数据,关系型数据库显的更为合适
2)、不适合持久存储海量数据
3)、无事务处理;
高并发下的数据缓存:
1、高并发下做数据缓存的原因:
当一个业务接口被用户调用后,系统首先会进入对应业务方法中进行逻辑运算,之后会根据逻辑运算结果,访问对应的数据SQL语句,对数据进行调用或者修改。这一系列的业务流程走完是需要时间的。但在面对高并发的环境下,哪怕是一毫秒的反映时间,都是至关重要。如何提高业务接口的交互效率,这里就可以使用数据缓存。缓存主要有redis和Cache两种
2、Redis、Cache与Memcached:
(1)、Redis是一个内存中的键值数据库
(2)、cached是一个高性能的分布式内存对象缓存系统,属于单机缓存
(3)、memcached是一个缓存框架,是对 cache 的二次封装
3、Redis与Memecache的区别:
(1)、存储方式:
1)、memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小
2)、Redis有部份存在硬盘上,这样能保证数据的持久性。
(2)、数据支持类型:
redis在数据支持上要比memecache多的多。
(3)、使用底层模型不同:
新版本的Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
(4)、运行环境不同:
Redis目前官方只支持LINUX 上去行,从而省去了对于其它系统的支持,这样的话可以更好的把精力用于本系统环境上的优化,虽然后来微软有一个小组为其写了补丁。但是没有放到主干上
注:有持久化需求或者对数据结构和处理有高级要求的应用,选择Redis。其他简单的key/value存储,选择cache。
Redis概述:
Redis是基于内存运行并支持持久化、高性能的NoSQL(非关系型)数据库,适用于存储频繁访问,数据量较小的数据,应用在配合关系型数据库做高速缓存与多样的数据结构存储持久化数据;
Redis的单线程模式:
1、Redis的单线程是指网络请求模块使用一个线程,所以不需考虑并发的安全性,对于需要依赖多个操作的复合操作来说,还是需要结合相应的锁机制。
2、Redis基于内存实现,同时CPU不是其性能瓶颈,所以采用单线程方案,避免了多线程的上下文切换操作,其多路I/O复用的线程模型,使得Redis的执行速度非常快。
Redis在Linux中的启动与关闭:
1、前台:(不推荐)
前台启动,命令行窗口不能关闭,否则服务器停止
(1)、启动:redis-server (2)、退出:ctrl+z (3)、查看pid进程:ps -elf | grep redis (4)、关闭:kill -9 <pid号>
2、后台:
修改redis.conf文件将里面的daemonize no改成yes,让服务在后台启动
注:建议备份redis.conf(在redis安装目录:/usr/local/bin/下执行)
(1)、后台启动:redis-server /备份路径/redis.conf (2)、客户端访问(默认端口6379):redis-cli 注: 1)、(指定端口访问) redis-cli –p端口号 2)、(密码连接) redis-cli –h <ip地址> –p <端口号> -a <密码> (3)、退出客户端:exit/quit (4)、关闭:redis-cli shutdown 注:(指定端口关闭) redis-cli –p端口号 shutdown
3、开启本地6379端口(可视化工具连接)
Redis基本命令:
1、查看Redis服务是否正常运行:
ping
#若正常则返回pong
2、查看Redis服务器的统计信息:
#查看Redis服务的所有统计信息
info
#查看Redis服务的指定统计信息
info [信息段]
3、测试Redis服务的性能:
redis-benchmark
4、Redis数据库实例
(1)、Redis实例默认创建了16个数据库,且不支持自定义命名,以dbX的方式命名(db0~db15),默认情况下,Redis客户端连接的是编号为0的数据库实例;
(2)、数据库的数量可以在配置文件中修改,不同的数据库可用于存储不同环境的数据;
(3)Redis集群下只有db0,不支持多db;
#切换数据库实例
select <index>
5、查看当前数据库实例中所有key的数量:
dbsize
6、查看当前数据库实例中的key:
备注:Redis中无论什么数据类型,在数据库中都是以key-value形式保存 |
|
keys <pattern> |
|
pattern中通配符 |
说明 |
* |
匹配0个或者多个字符 |
? |
匹配1个字符 |
[ ] |
匹配[]里边的1个字符 |
案例 |
|
keys * |
查看数据库中所有的key |
keys a* |
查看数据库中所有以a开头的key |
keys h?o |
查看数据库中所有以h开头、以o结尾的、并且中间只有一个字符的key |
keys h[abc]llo |
查看数据库中所有以h开头以llo结尾,并且h后边只能取abc中的一个字符的key |
7、判断key在数据库中是否存在:
exists <key> |
如果存在,则返回1;如果不存在,则返回0 |
exists <key1> <key2> …… |
返回值是存在的key的数量 |
8、移动指定key到指定的数据库:
move <key> <index>
9、查看指定key的剩余生存时间:
ttl <key> |
说明: |
如果key没有设置生存时间,返回-1 |
|
如果key不存在,返回-2 |
10、设置key的最大生存时间(过期时间):
expire <key> <seconds>
11、查看指定key的数据类型:
type <key>
12、重命名key:
rename <key> <newkey>
13、删除指定的key:
del key [key key .....] |
返回值是实际删除的key的数量 |
14、清空数据库实例:
flushdb
15、清空所有的数据库实例:
flushall
16、Redis的配置信息:
#查看Redis中所有的配置信息: config get * #查看Redis中的指定的配置信息: config get <parameter> #编辑配置信息 Config set <parameter> <new_config_value>
Redis特点:
一、支持存储多种数据类型:
在Redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。
1、String字符串:
(1)、单键-单值
(2)、String类型是Redis最基本的数据类型,是二进制安全的,一个Redis中字符串value最多可以是512M
(3)、操作命令:
(4)、数据结构:
(5)、应用:
计数器、对象存储缓存
2、List列表:
(1)、单键-有序可重复多值
(2)、一个key对应多个value,多个value之间有顺序且可重复,最左侧是表头,最右侧是表尾,可以在左右两边插入弹出;
(3)、每一个元素都有下标,表头元素的下标是0,依次往后排序,最后一个元素下标是列表长度-1
(4)、每一个元素的下标又可用负数表示,表尾元素下标用-1,依次往前排
(5)、操作:
命令 |
描述 |
LPUSH/RPUSH key value1[value2..] |
从左边/右边向列表中PUSH值(一个或者多个) |
LRANGE key start end |
获取list 起止元素 |
LPUSHX/RPUSHX key value |
向已存在的列名中push值(一个或者多个) |
LINSERT key BEFORE|AFTER pivot value |
在指定列表元素的前/后插入value |
LTRIM key start end |
通过下标截取指定范围内的列表 |
(6)应用:
消息排队、消息队列、栈
3、set集合
(1)、单键-无序不可重复多值
(2)、Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)
(3)、操作:
命令 |
描述 |
SADD key member1[member2..] |
向集合中无序增加一个/多个成员 |
SCARD key |
获取集合的成员数 |
SMEMBERS key |
返回集合中所有的成员 |
SISMEMBER key member |
查询member元素是否是集合的成员,结果是无序的 |
SRANDMEMBER key [count] |
随机返回集合中count个成员,count缺省值为1 |
4、Hash哈希:
(1)、键值对集合
(2)、Redis hash是一个String类型的field和value的映射表(单key:[field-value])
(3)、通过key+field(属性标签)就可以操作对应value属性数据,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题
(4)、操作:
命令 |
描述 |
HSET key field value |
将哈希表 key 中的字段 field 的值设为 value 。重复设置同一个field会覆盖,返回0 |
HMSET key field1 value1 [...] |
同时将多个 field-value (域-值)对设置到哈希表 key 中 |
HSETNX key field value |
只有在字段 field 不存在时,设置哈希表字段的值 |
HEXISTS key field |
查看哈希表 key 中,指定的字段是否存在 |
HGET key field value |
获取存储在哈希表中指定字段的值 |
(5)、应用:
适用于存储对象
5、Zset有序集合
(1)、本质上是有序集合,所有元素不能重复
(2)、每一个元素都会关联一个double类型的分数(score)。Redis正是通过分数来为集合中的成员进行从小到大的排序
(3)、zset的元素(value)是唯一的,但分数(score)却可重复
(4)、操作:
命令 |
描述 |
ZADD key score member1 [score2 member2] |
向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
ZCARD key |
获取有序集合的成员数 |
ZCOUNT key min max |
计算在有序集合中指定区间score的成员数 |
ZRANGE key start end |
通过索引区间返回有序集合成指定区间内的成员 |
ZRANK key member |
返回有序集合中指定成员的索引 |
6、BitMaps位图
(1)、位存储,信息状态只有0和1,未设置默认为0
(2)、结构:key:offset-value
(3)、Bitmap本身是一个字符串,数组的每个单元(value)只能存放0和1,数组的下标在Bitmaps叫做偏移量(offset);
(4)、操作:
命令 |
描述 |
setbit key offset value |
为指定key的offset位设置值 |
getbit key offset |
获取offset位的值 |
bitcount key [start end] |
统计字符串被设置为1的bit数,也可以指定统计范围按字节 |
bitop operration destkey key[key..] |
对一个或多个保存二进制位的字符串key进行位元操作,并将结果保存到 destkey 上。 |
BITPOS key bit [start] [end] |
返回字符串里面第一个被设置为1或者0的bit位。start和end只能按字节,不能按位 |
(5)、应用:
用户签到、统计活跃数多的用户、用户在线状态(在线就设置为1,不在线就设置为0)
7、HyperLogLog基数统计
(1)、基数:数据集中不重复的元素的个数,例A(1,3,5,7,8,7) 的基数是5
(2)、Hyperloglog是基数统计的算法,只会根据输入元素来计算基数,而不会储存输入元素本身,所以内存固定,占用内存很小(12KB)
(3)、底层使用String数据类型
(4)、操作:
命令 |
描述 |
PFADD key element1 [elememt2..] |
添加指定元素到 HyperLogLog中 |
PFCOUNT key [key] |
返回给定 HyperLogLog 的基数估算值 |
PFMERGE newkey sourcekey [sourcekey..] |
将多个HyperLogLog合并 |
(5)、应用:
网页的访问量UV统计(同一用户多次访问)
8、Geospatial地理位置
(1)、Redis3.2 版本开始推出的Geospatial,使用经纬度定位地理坐标并用一个有序集合zset保存
(2)、有效经纬度
1)、有效的经度:从-180度到180度。
2)、有效的纬度:从-85.05112878度到85.05112878度
(4)、操作:
命令 |
描述 |
geoadd <key> <longitude> <latitude> member [<longitude> <latitude> member ...] |
添加地理位置 longitude:经度,latitude:纬度,名称 |
geopos key member [member..] |
获取集合中的一个/多个成员坐标 |
geodist key member1 member2 [unit] |
返回两个给定位置之间的距离 [unit]:默认米(m),还包括有km/mi/ft |
georadius<key> <longitude> <latitude> radius [unit] [withcoord][ withdist] |
获取两个位置之间的直线距离 withcoord:返回带坐标 withdist:返回带距离 |
(5)、应用:
朋友定位(geopos)、查看附近的人(georadius)、打车距离计算(geodist)
二、支持数据持久化:
可将Redis内存中的数据持久化到磁盘中,重启时可再次加载进行使用
1、RDB策略:
(1)、定义:
在指定的时间间隔内将内存中的数据集(Snapshot)快照写入磁盘,恢复时是将快照文件直接读到内存里。(Redis默认开启)
相关配置:redis.conf |
描述 |
save <秒钟> <写操作次数> |
配置持久化策略 |
dbfilename |
配置RDB持久化数据存储的文件名 |
dir |
配置RDB持久化文件所在目录 |
(2)、工作原理:
Redis会单独创建一个子进程(fork)来进行持久化,先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。
(3)、触发机制:
1)、save的规则满足的情况下,即:在指定时间间隔内,redis服务执行指定次数的写操作,会自动触发RDB原则
2)、执行flushall命令,也会触发我们的RDB原则
3)、退出Redis,也会自动产生RDB文件
注:
Bgsave:异步进行,进行持久化的时候,Redis还可以将继续响应客户端请求
命令 |
save |
bgsave |
IO类型 |
同步 |
异步 |
阻塞 |
是 |
是(阻塞发生在fock(),通常非常快) |
复杂度 |
O(n) |
O(n) |
优点 |
不会消耗额外的内存 |
不阻塞客户端命令 |
缺点 |
阻塞客户端命令 |
需要fock子进程,消耗内存 |
(4)、恢复机制:
只需要将RDB文件放在Redis启动目录下,Redis启动的时候会自动检查RDB文件并恢复其中的数据
(5)、优缺点:
优点:
1)、适合大规模的数据恢复
2)、对数据的完整性要求不高
缺点:
1)、需要一定的时间间隔进行操作,如果Redis意外宕机了,这个最后一次修改的数据就没有了
2)、fork进程的时候,会占用一定的内容空间
2、AOF策略:
(1)定义:
采用操作日志来记录(追加到日志文件末尾)每次对服务器的写操作,每次Redis服务启动时,都会重新从前到后执行一遍操作日志中的指令。效率较低,Redis默认不开启AOF功能
相关配置 |
描述 |
appendonly no |
默认是不开启AOF模式的,默认是使用RDB方式持久化 |
appendfilename "appendonly.aof" |
配置文件名称,默认为 appendonly.aof |
appendfsync always/everysec/no |
同步频率机制: (1)、always:每次修改都会sync 消耗性能 (2)、everysec:每秒执行一次sync可能会丢失这一秒的数据 (3)、no:不执行 sync ,这时候操作系统自己同步数据,速度最快 |
(2)、Rewrite重写(压缩):
1)、AOF文件越来越大,需要定期对AOF文件进行重写达到压缩
2)、旧的AOF文件含有无效命令会被忽略,保留最新的数据命令
3)、多条写命令可以合并为一个
4)、AOF重写降低了文件占用空间
5)、更小的AOF文件可以更快的被Redis加载
(3)、Rewrite重写原理:
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再 rename),redis4.0版本后的重写,是指上就是把rdb的快照,以二级制的形式附在新的aof 头部,作为已有的历史数据,替换掉原来的流水账操作。
涉及相关配置 |
描述 |
no-appendfsync-on-rewrite <yes> |
不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能) |
no-appendfsync-on-rewrite <no> |
还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低) |
(4)、重写触发配置:
1)、手动触发:
直接调用bgrewriteaof命令
2)、自动触发
相关配置: |
描述 |
auto-aof-rewrite-min-size <64mb> |
AOF文件最小重写大小,只有当AOF文件大小大于该值时候才能重写,6.x版本默认配置64MB |
auto-aof-rewrite-percentage <100> |
当前AOF文件大小和最后一次重写后的大小之间的比率等于或者大于指定的增长百分比,如100代表当前AOF文件是上次重写的两倍时候才重写 |
(5)、优缺点:
优点:
1)、备份机制更稳健,丢失数据概率更低
2)、可读的日志文本,通过操作AOF稳健,可以处理误操作
缺点:
1)、比起RDB占用更多的磁盘空间。
2)、恢复备份速度要慢。
3)、每次读写都同步的话,有一定的性能压力。
3、总结
(1)、如果对数据不敏感,可以选单独用RDB;
(2)、不建议单独用AOF,因为可能会出现Bug;
(3)、如果只是做纯内存缓存,可以都不用;
(4)、同时开启两种持久化方式时,在这种情况下,当Redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
三、支持数据备份:
1、主从(Master/Slave)复制模式:
(1)、理解:
主少从多、主写从读、读写分离、主写同步复制到从,默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点
(2)、相关操作:
1)、搭建主从集群架构:
修改相关配置文件:
#修改相关配置文件的格式: bind 127.0.0.1 port 6379 #端口 pidfile /var/run/redis_6379.pid #pid名 logfile "6379.log" #log文件名 dbfilename dump6379.rdb #.rdb名
2)、查看集群主从角色:
info replication #默认都为主机
3)、从机设置主从关系(设从不设主)
slaveof <主机ip> <主机端口>
#从机只能读(主机写)
#主库写数据会自动同步到从库
#读写分离
4)、从机断开原来主从关系:
slaveof no one
5)、主机宕机、从机原地待命,主机恢复,一切恢复正常
6)、从机宕机,主机少一个从机,其他从机不变 ;从机恢复,需重新设置主从关系
2、哨兵模式:
(1)、理解:
主机宕机,哨兵程序根据投票机制自动选择从机上位,当主机恢复,自动从属于新的主机
(2)、相关操作:
1)、搭建主从集群架构
2)、配置哨兵配置文件(redis_sentinel.conf):
sentinel monitor <被监控名> <主机ip> <主机端口> <投票数>
3)、启动哨兵服务:
redis-sentinel redis_sentinel.conf
Redis事务机制:
1、Redis事务定义:
(1)、事务:把一组数据库命令放在一起执行,保证操作原子性,要么同时成功,要么同时失败。
(2)、Redis的事务:允许把一组Redis命令放在一起,把命令进行序列化,然后按顺序地执行,保证部分原子性。
2、操作:
命令 |
描述 |
multi |
用来标记一个事务的开始 |
exec |
用来执行事务队列中所有的命令 |
discard |
清除所有已经压入队列中的命令,并且结束整个事务 |
watch <key> |
监控某一个键,当事务在执行过程中,此键代码的值发生变化,则本事务放弃执行;否则,正常执行(乐观锁操作) |
unwatch |
放弃监控所有的键 |
注:
(1)、乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。(版本控制)
(2)、悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。(事务排队执行)
(3)、每次提交执行exec后都会自动释放锁,不管是否成功
3、Redis事务的部分原子性:
(1)、如果一组命令中,有在压入事务队列过程中发生错误的命令,则本事务中所有的命令都不执行,能够保证事务的原子性
(2)、如果一组命令中,在压入队列过程中正常,但是在执行事务队列命令时发生了错误,则只会影响发生错误的命令,不会影响其它命令的执行,不能够保证事务的原子性
如何保证缓存和数据库数据的一致性:
1、合理设置缓存的过期时间。
2、新增、更改、删除数据库操作时同步更新Redis,可以使用事物机制来保证数据的一致性。
Redis发布与订阅:
1、定义:
(1)、Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
(2)、订阅/发布消息图:第一个:消息发送者、第二个:频道、第三个:消息订阅者
2、操作:
消息发布端: |
|
PUBLISH <channel> <message> |
向指定频道发布消息 |
|
|
订阅端: |
|
SUBSCRIBE <channel> [channel..] |
订阅给定的一个或多个频道 |
UNSUBSCRIBE <channel> [channel..] |
退订一个或多个频道 |
|
|
频道 |
|
PUBSUB channels |
查看活跃频道 |
3、缺点:
(1)、订阅端读取消息的速度不够快,那么积压的消息会使Redis输出缓冲区的体积变得越来越大,这可能使得Redis本身的速度变慢,甚至直接崩溃
(2)、订阅端断线,将会丢失所有在短线期间消息发布端发布的消息
4、应用:
消息订阅:公众号订阅,微博关注等(消息队列)、多人在线聊天室
缓存机制
一、缓存穿透:
1、理解(即查询不到):
在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。(洪水攻击)
2、解决方案:
(1)、布隆过滤器:
对所有可能查询的参数以Hash的形式存储到bitmap,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,从而减轻底层存储系统的压力
(2)、设置空值缓存与过期时间:
如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但由于其默认过期时间很短,通过设置其过期时间存放到缓存中,这样下次到缓冲中就可直接获取到该值,而不会继续访问数据库
//伪代码 public object GetProductListNew(){ int cacheTime=30; String cacheKey="product_list"; String cacheValue=CacheHelper.Get(cacheKey); if(cacheValue!=null){ return cacheValue; } cacheValue=CacheHelper.Get(cacheKey); if(cacheValue!=null){ return cacheValue; }else{ //数据库查询不到,为空 cacheValue=GetProductListFromDB(); if(cacheValue==null){ //如果发现为空,设置个默认值,也缓存起来 cacheValue=string.Empty; } CacheHelper.Add(cacheKey,cacheValue,cacheTime); return cacheValue; } }
(3)、实时监控,禁止其访问
二、缓存击穿:
1、理解(量过大,缓存过期):
一个存在的key,在不停的扛着大并发请求,请求集中对这一点进行访问,在缓存过期的一刻,持续的大并发请求就穿破缓存,直接请求在数据库,这个时候大并发的请求可能会瞬间将后端DB压垮。
2、解决方案:
(1)、设置热点的数据永不过期
当Redis内存空间满的时候会清理部分数据,而且一旦热点数据过多,此种方案会占用空间
(2)、加互斥锁
让一个线程回写缓存,其他线程等待回写缓存线程执行完,重新读缓存即可
注:
分布式应用就需要使用分布式锁
//伪代码 static Lock reenLock = new ReentrantLock(); public List<String> getData04()throws InterruptedException{ List<String> result=new ArrayList<String>(); // 从缓存读取数据 result=getDataFromCache(); if(result.isEmpty()){ if(reenLock.tryLock()){ try{ System.out.println("拿到锁,从DB获取数据库后写入缓存"); // 从数据库查询数据 result=getDataFromDB(); // 将查询到的数据写入缓存 setDataToCache(result); }finally{ reenLock.unlock();// 释放锁 } }else{ result=getDataFromCache();// 先查一下缓存 if(result.isEmpty()){ System.out.println("没拿到锁,缓存也没数据,先小憩一下"); Thread.sleep(100);// 小憩一会儿 return getData04();// 重试 } } } return result; }
三、缓存雪崩
1、理解(缓存集中过期失效):
大量的key设置了相同的过期时间,导致缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩
2、解决方案:
(1)、加锁排队:
跟缓存击穿解决思路一致,同一时间只让一个线程构建缓存,其他线程阻塞排队,不适用高并发情况
(2)、缓存标记:
给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存
注:
1)、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存。
2)、缓存数据:其过期时间比缓存标记的时间延长一倍
//伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; //缓存标记 String cacheSign = cacheKey + "_sign"; String sign = CacheHelper.Get(cacheSign); //获取缓存值 String cacheValue = CacheHelper.Get(cacheKey); if (sign != null) { return cacheValue; //未过期,直接返回 } else { CacheHelper.Add(cacheSign, "1", cacheTime); ThreadPool.QueueUserWorkItem((arg) -> { //这里一般是 sql查询数据 cacheValue = GetProductListFromDB(); //日期设缓存时间的2倍,用于脏读 CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2); }); return cacheValue; } }
四、缓存预热
1、理解(缓存数据提前加载):
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候,先查询数据库,然后再将数据回写到缓存。
2、解决方案:
(1)、直接写个缓存刷新页面,上线时手工操作下;
(2)、数据量不大,可以在项目启动的时候自动进行加载;
(3)、定时刷新缓存
五、缓存更新
1、理解(自定义缓存淘汰):
除了缓存服务器自带的缓存失效策略之外,还可以根据具体的业务需求进行自定义的缓存淘汰
2、解决方案:
(1)、定时去清理过期的缓存。
(2)、当有用户请求过来时,先判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
六、缓存降级:
1、理解(弃卒保帅):
缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度,其最终目的是保证核心服务可用。
2、解决方案:
在项目实战中通常会将部分热点数据缓存到服务的内存中,这样一旦缓存出现异常,可以直接使用服务的内存数据,从而避免数据库遭受巨大压力。
Redis数据的过期回收策略与内存淘汰机制:
1、过期回收策略:
Redis中的数据过期回收策略使用了定期删除和惰性删除相结合的方式。
(1)、定期删除:Redis会每隔一定的时间去抽查一定量的数据判断其是否过期,过期则进行删除。
(2)、惰性删除:在获取一个key的时候,Redis会检查这个key是否已经过期,若过期,则会进行删除操作。
2、内存淘汰机制:
在配置文件中,可以对内存淘汰机制进行配置。当Redis内存数据使用达到最大值时,就会施行数据淘汰策略,Redis可以使用的淘汰策略如下:
(1)、volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
(2)、allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。
(3)、volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
(4)、allkeys-random:从数据集中任意选择数据淘汰。
(5)、volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
(6)、noeviction :禁止驱逐数据,默认选项
Redis常见的性能问题与解决方案:
1、Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次
(1)、Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务
(2)、Master进行AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。而重写AOF文件会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
2、Redis主从复制的性能问题
为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内
3、避免在压力很大的主库上增加从库
Springboot整合Redis:
(参考链接 提取码:3xne)
一、短信验证功能:
(参考一)
(参考二)
思路: 1、发送UUID短信,code存储到Redis,设置到期时间 //redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS) redisTemplate.opsForValue().set(redisKey,code,300, TimeUnit.SECONDS); 2、注册验证 String realCode= (String) redisTemplate.opsForValue().get(redisKey); //判断前端所传值与Redis缓存的值是否相同(缓存未失效) if (realCode!=null&&realCode.equals(inputCode)){}
二、秒杀机制功能:
(参考一)
(参考二)
秒杀:商家在某特定时间段里大幅降低网络商品价格的一种营销活动 思路: 1、初始化接口,将MySQL底层查询的库存量写入Redis缓存 //设置过期时间,【可传入库存量】 redisTemplate.opsForValue().setIfAbsent(商品ID, 商品库存量, 过期时间, TimeUnit.MINUTES); 2、Redis缓存秒杀接口 //可传入商品ID、userID(单人限购买) //lua脚本秒杀、synchronized单机秒杀、分布式锁 if(redisTemplate.opsForValue().decrement(商品ID) > -1){} 3、定时任务将Redis缓存剩余库存数写入MySQL String articleStockNum = (String)redisTemplate.opsForValue().get(商品ID);
三、Redis分布式锁实现机制:
(参考一)
(参考二)
1、分布式锁:
控制分布式系统之间同步访问共享资源的一种方式。即在系统里面占一个“坑”,其他程序也要占“坑”的时候,占用成功了就可以继续执行,失败了就只能放弃或稍后重试
2、分布式锁实现机制:
(1)、基于数据库实现分布式锁;
(2)、基于缓存(Redis等)实现分布式锁;
(3)、基于Zookeeper实现分布式锁
3、基于Redis实现分布式锁
(1)、普通实现:
setnx+expire+lua实现,或者采用set复合命令+lua实现
1)、实现的三大要点:
【1】、set命令使用set key value px milliseconds nx:替代 setnx + expire 需要分两次执行命令的方式,保证了原子性。
【2】、value要具有唯一性:可以使用UUID.randomUUID().toString()方法生成,用来标识这把锁是属于哪个请求加的,在解锁的时候就可以有依据;
【3】、释放锁时要验证value值,不能误解锁:同时利用了eval命令执行Lua脚本的原子性
2)、缺点:
最大的缺点:加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况,导致出现多个客户端持有锁的情况,这样就不能实现资源的独享了
注:
【1】、
setIfAbsent:java中的方法
setnx:redis命令中的方法
如果为空就set值,并返回1;如果存在(不为空)不进行操作,并返回0。
【2】、
Redis 2.6.12版本起, 提供可选的字符串set复合命令:
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
可选参数如下:
EX: 设置超时时间,单位是秒
PX: 设置超时时间,单位是毫秒
NX: IF NOT EXIST 的缩写,只有 KEY不存在的前提下 才会设置值
XX: IF EXIST 的缩写,只有在 KEY存在的前提下 才会设置值
(2)、Redlock算法实现:
在锁失效时间、获取锁超时时间、锁使用时间正常情况下,redlock算法认为,只要N/2+1个节点加锁成功,那么就认为获取了锁,解锁时将所有实例解锁。(Redisson实现红锁Redlock)
(3)、Redisson框架实现:
(参考)
Redisson是一个高级的分布式协调Redis客服端,提供了一系列的分布式的Java常用对象,还提供了许多分布式服务
1)、Redisson所有指令都通过lua脚本执行,保证了操作的原子性
2)、Redisson设置了watchdog看门狗,“看门狗”的逻辑保证了没有死锁发生
四、哨兵模式功能
五、备注:
1、Springboot 2.x后 ,由lettuce客户端替换Jedis客户端来连接Redis
客户端 |
描述 |
Jedis |
采用的直连,多个线程操作的话,是不安全的。如果要避免不安全,使用jedis pool连接池!更像BIO模式 |
lettuce |
采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式 |
Redisson |
采用了基于NIO的Netty框架,提供了一系列的分布式的Java常用对象,还提供了许多分布式服务 |
2、spring-data-redis针对jedis提供了如下功能:
(1)、连接池自动管理,提供了一个高度封装的“RedisTemplate”类
(提供redis各种操作、异常处理及序列化,支持发布订阅,对spring 3.1 cache进行了实现)
(2)、针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口
ValueOperations |
简单K-V操作 |
SetOperations |
set类型数据操作 |
ZSetOperations |
zset类型数据操作 |
HashOperations |
针对map类型的数据操作 |
ListOperations |
针对list类型的数据操作 |
(3)、提供了对key的“bound”(绑定)便捷化操作API,可以通过bound封装指定的key,然后进行一系列的操作而无须“显式”的再次指定Key,即BoundKeyOperations
(4)、将事务操作封装,有容器控制。
(5)、针对数据的“序列化/反序列化”,提供了多种可选择策略(RedisSerializer)