在了解Redis之前,先要了解下面一个问题:
我们先来看一下一个简单的应用是怎么工作的。
简单的应用.jpg但是这样的应用有两方面缺点
高并发场景.jpg
- 性能问题:对有些数据的查询操作十分费时,并且这些数据经常被查询,就会影响性能。
- 并发问题:高并发的情况下,所有的请求都直接访问数据库,会导致数据库承受不住那么大的压力出现故障。
为了缓解这两个方面的问题,我们在服务器与数据库之间多加了一个缓存,为什么用了缓存就可以缓解以上两种情况呢?
- 对于那些访问频繁的数据,直接从缓存中获取,不需要去数据库中执行比较耗时的SQL。
- 高并发场景下,一部分请求在缓存中获取到了数据,不需要再访问数据库了。
Redis就是一种运行速度很快,基于内存也可以持久化数据的NoSql数据库。因为速度快,所以被用来作为上面说的缓存。
Redis为什么快?
主要有以下三点:
- 纯内存操作,对内存的操作自然快。
- 单线程,避免了频繁的线程上下文切换。
- 采用了非阻塞I/O多路复用机制。
Redis常见的数据结构
字符串
字符串是Redis最基本的数据结构,它将以一个键和一个值存储于Redis内部,让Redis通过 键 去找到 值。
使用场景
- 计数,比如播放量。
- 限制过期时间,比如短信验证码的有效时间。
哈希表
Redis中哈希结构就如同Java的map一样,一个对象里面有许多键值对,它是特别适合存储对象的。
使用场景
- 储存对象,Key表示对象,Field表示对象的属性名,Value表示属性值。
- 共享Session。
列表
链表结构是Redis中一个常用的结构,它可以存储多个字符串,而且它是有序的。
Redis链表是双向链表,所以只能够从左到右,或者从右到左地访问和操作链表里面的数据节点。
使用链表结构就意味着读性能的丧失,所以要在大量数据中找到一个节点的操作性能是不佳的。
使用场景
- 消息队列
- 可以利用lrange命令,做基于redis的分页功能。
集合
Redis的集合不是一个线性结构,而是一个哈希表结构,它的内部会根据hash分子来存储和查找数据。
因为采用哈希表结构,所以对于Redis集合的插入、删除和查找的复杂度都是O(1)。
我们需要注意3点
- 它的每一个元素都是不能重复的,当插入相同记录的时候都会失败。
- 集合是无序的。
- 集合的每一个元素都是String数据结构类型。
有序集合
有序集合和集合类似,只是说它是有序的,和无序集合的主要区别在于每一个元素除了值之外,它还会多一个分数。
分数是一个浮点数,在Java中是使用双精度表示的,根据分数,Redis就可以支持对分数从小到大或者从大到小的排序。
这里和无序集合一样,对于每一个元素都是唯一的,但是对于不同元素而言,它的分数可以一样。
元素也是String数据类型,也是一种基于跳表的存储结构。
使用场景
- 排行榜应用
- 取TOP N
- 延时任务
- 范围查找。
Redis的事务
在Redis中开启事务是multi命令,而执行事务是exec命令。multi到exec命令之间的Redis命令将采取进入队列的形式,直至exec命令的出现,才会一次性发送队列里的命令去执行,而在执行这些命令的时候其他客户端就不能再插入任何命令了,这就是Redis的事务机制。
注意 Redis中的事务与数据库很不一样
- 在执行事务命令的时候,在命令入队的时候,Redis就会检测事务的命令格式是否正确,如果不正确则会产生错误,此时队列里的所有命令都不会执行。
- 当命令格式正确,而因为操作数据结构引起的错误,则该命令执行出现错误,而其之前和之后的命令都会被正常执行。
watch 监控变化
在Redis中使用watch命令可以决定事务是执行还是回滚。
- 可以在multi命令之前使用watch命令监控某些键值对。
- 使用multi命令开启事务,执行各类对数据结构进行操作的命令,这个时候这些命令就会进入队列。
- 当Redis使用exec命令执行事务的时候,它首先会去比对被watch命令所监控的键值对,
如果没有发生变化,那么它会执行事务队列中的命令,提交事务;
如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。- 无论事务是否回滚,Redis都会去取消执行事务前的watch命令。
Redis持久化
在Redis中存在两种方式的备份:
- 快照(snapshotting):它是备份当前瞬间Redis在内存中的数据记录。
- 只追加文件(Append-OnlyFile,AOF):其作用就是当Redis执行写命令后,在一定的条件下将执行过的写命令依次保存在Redis的文件中,将来就可以依次执行那些保存的命令恢复Redis的数据了。
对于快照备份而言,如果当前Redis的数据量大,备份可能造成Redis卡顿,但是恢复重启是比较快速的;
对于AOF备份而言,它只是追加写入命令,所以备份一般不会造成Redis卡顿,但是恢复重启要执行更多的命令,备份文件可能也很大,使用者使用的时候要注意。
在Redis中允许使用其中的一种、同时使用两种,或者两种都不用,所以具体使用何种方式进行备份和持久化是用户可以通过配置决定的。
持久化相关配置
快照
// 满足“90秒内至少有1个键被改动”这一个条件时,自动保存一次数据集。
save 900 1
save 300 10
save 60 10000
/*
这里先谈谈bgsave命令,它是一个异步保存命令,也就是系统将启动另外一条进程,把Redis的数据保存到对应的数据文件中。它和save 命令最大的不同是它不会阻塞客户端的写入,也就是在执行bgsave的时候,允许客户端继续读/写Redis。在默认情况下,如果Redis执行 bgsave失败后,Redis将停止接受写操作,这样以一种强硬的方式让用户知道数据不能正确的持久化到磁盘,否则就会没人注意到灾难的 发生,如果后台保存进程重新启动工作了,Redis也将自动允许写操作。
*/
stop-writes-on-bgsave-error yes
// 是否对rdb文件进行校验
rdbchecksum yes
// 快照文件的名字
dbfilename dump.rdb
AOF
// 开启AOF
appendonly no
// AOF文件名
appendfilename "appendonly.aof"
// 每条Redis命令都同步到AOF
appendfsync always
// 每秒同步一次
appendfsync everysec
// 不同步
appendfsync no
// 是否在rewrite过程中禁止调用appendfsync
no-appendfsync-on-rewrite no
// 触发rewrite的阈值 0则禁用
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
Redis的过期删除策略以及内存淘汰机制
过期删除策略
Redis采用定期删除+惰性删除来删除过期的Key.
定期删除:Redis默认每个100ms随机检查一部分Key,判断他是否过期了,过期就删除。这样没被检查到的过期数据就不会被删除。
惰性删除:在获取某个Key的时候,判断这个Key是否过期,过期就删除。
此时还会有漏网之鱼,即没有被检查到,也没有被访问到的过期数据。
内存淘汰机制
如果Redis的内存不够了,那么就会采用内存淘汰机制进行清理。
maxmemory-policy volatile-lru
- noeviction:根本就不淘汰任何键值对,当内存已满时,如果做读操作,例如get命令,它将正常工作,而做写操作,它将返回错误。也就是说,当Redis采用这个策略内存达到最大的时候,它就只能读而不能写了。
- allkeys-random:采用随机淘汰策略删除所有的(不仅仅是超时的)键值对,这个策略不常用。
- allkeys-lru:采用淘汰最少使用的策略,Redis将对所有的(不仅仅是超时的)键值对采用最近使用最少的淘汰策略
- volatile-lru:采用最近使用最少的淘汰策略,Redis将回收那些超时的(仅仅是超时的)键值对,也就是它只淘汰那些超时的键值对。
- volatile-random:采用随机淘汰策略删除超时的(仅仅是超时的)键值对。
- volatile-ttl:采用删除存活时间最短的键值对策略。
使用Redis的注意事项
数据库缓存双写不一致
使用分布式就会有数据不一致的情况。
先更新缓存再更新数据库(不要用这个方法)
- 更新缓存成功
- 更新数据库失败,事务回滚。
此时缓存中是脏数据。
先更新数据库再更新缓存(不要用这个方法)
用这种方法会出现两方面问题
(一)线程安全
- A线程更新了数据库。
- B线程更新了数据库。
- B线程更新了缓存。
- A线程更新了缓存。
此时B线程对缓存的更新就丢失了。
(二)资源浪费
- 如果缓存中的数据八百年没人访问,但是你每次修改都去更新,不如删了,下次需要读取再重新读呗
- 有的时候数据库读出的数据还得加工才能放入缓存,每次修改都得对数据加工。
先删除缓存再更新数据库
- A线程删除了缓存中的数据。
- B线程查询数据,发现缓存中没有数据。
- B线程从数据库中读取旧数据,并放入缓存。
- A线程更新数据库。
此时缓存中是B读取出来的旧数据,并且如果缓存中的旧数据不过期,那么之后读取的都是旧数据。
可以通过异步串行化解决
先更新数据库再删除缓存(Cache aside)
(一)线程安全
- B线程读取从数据库中读取旧数据。
- A线程更新数据库。
- A线程删除缓存。
- B线程将旧数据更新至缓存。
此时缓存中是旧数据。
(二)删除缓存失败
此时缓存中是旧数据
缓存穿透
缓存穿透,即攻击者故意去请求缓存中不存在的数据,导致所有的请求都直接打到数据库上,从而出现问题。
解决方案:
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。
- 缓存一个空对象,设置短一点的过期时间。
- 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
缓存雪崩
缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了大量请求,直接访问数据库,导致数据库压力过大,出现问题。
解决方案:
- 给缓存的失效时间,加上一个随机值,避免集体失效。
- 互斥锁。
缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
- 设置热点数据永远不过期。
- 互斥锁。
网友评论