Redis简介
# Redis简介
# 1 简单介绍Redis
Redis时一个用C语言开发的数据库,与传统的数据库不同,redis的数据存储在内存中,所以读写数据特别快,所以可以应用在一下几个方面:
- 缓存:通过Redis来做分布式锁是一种比较常见的方式。
- 限流:一般是通过Redis + Lua脚本来实现限流。
- 分布式锁,甚至消息队列
# 2 使用Redis缓存的原因
使用缓存主要是为了提升用户体验以及能应对更多的用户。主要有两个用途:高性能、高并发
⭐ 高性能:
假如用户第一次访问数据库的某些数据的话,这个过程是比较慢的,毕竟是从硬盘读取。但是,如果用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。当用户下次再访问这些数据的时候就可以直接从缓存中获取,从缓存中读取的速度十分快。(但要保证数据库和缓存中的数据一致性)。
⭐ 高并发:
一般MySQL这类数据库的 QPS 大概为 1W 左右(4核8g),但是使用 Redis 缓存之后容易达到 10W,甚至 30w。
QPS,Quey Per Second,服务器每秒可执行的查询次数。
# 3 缓存数据的处理流程
- 如果用户请求的数据再缓存中就直接返回。
- 缓存中不存在的话就看数据库中是否存在。
- 数据库中存在的话就更新缓存中的数据。
- 数据库中不存在的话就返回空数据。
# 4 缓存与数据库一致性
⭐ Cache Aside Pattern
最经典的缓存 + 数据库读写的模式,就是 Cache Aside Pattern
- 读的时候,先读缓存,缓存没有再读数据库,取出数据后放入缓存,同时返回响应
- 更新的时候,先删除缓存,再更新数据库
⭐ 为什么是删除而不是更新?
- 复杂场景下,缓存不单单是数据库直接取出来的值。比如更新某一个表的一个字段,但它对应的缓存需要两个表的数据进行计算。
- 数据库经常修改,但缓存不经常被访问,这种情况更新缓存就浪费了。
删除缓存而不是更新缓存,有点懒加载的思想。
# 5 简单的缓存不一致问题
问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路1:先删除缓存,再更新数据库。如果数据库更新失败,那么数据库中是旧数据,缓存中是空的,那么就不会不一致。因为读不到缓存就会去数据库中读取旧数据并更新到缓存中。
解决思路2:延时双删。先更新数据库再删除缓存,过一会再删除一次缓存。
public void set(key, value) {
putToDb(key, value);
deleteFromRedis(key);
// ... a few seconds later
deleteFromRedis(key);
}
2
3
4
5
6
7
# 6 复杂的缓存不一致问题
参考:advanced-java (opens new window)
问题:数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了...
解决思路:更新数据时,根据数据的唯一标识,将操作发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将读取数据的操作也会发送到同一个 jvm 内部队列中。一个队列对应一个线程串行工作,这样就会保证删除缓存和更新数据库的操作之间不会插入读取数据库的操作了。
# 7 Redis并发竞争
问题:某一时刻,多客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本出错;
解决:基于 zookeeper 实现分布式锁,确保同一时间只能由一个系统实例在操作某个key。
问题:多个客户端同时获取一个key,修改值后再写回Redis,只要写回的顺序错了,数据就错了。
解决思路:乐观锁,每次写之前判断这个 value 的时间戳是否比缓存里的 value 的时间戳新,如果新的话才可以写。