什么是Redis
- 内存数据库
- 非关系型数据库 (
数据库不需要被预定义
) - 有5种独特的数据类型
- 特性
- 可持久化
- 可复制 (
扩展读性能 & 提供故障转移
) - 可客户端分片 (
扩展写性能
)
Redis的好处
为什么 redis 单线程模型也能效率这么高?
- 纯内存操作
- 核心是基于非阻塞的 IO 多路复用机制 (
像Windows一样的事件处理机制
) - 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- Redis是基于内存的操作,CPU不是Redis的瓶颈 ,Redis的瓶颈最有可能是机器内存的大小或者网络带宽 (也就是就算采用多线程去榨干CPU资源,还是得等网络带宽)。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。
- 不过无法发挥多核CPU性能,但是可以通过在单机开多个Redis实例来完善;
- 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
对比memcached
- 相同点 :
- 都可用于存储key-value mapping
- 性能也相差无几
- Redis的优势 :
- 能够自动以两种不同的方式将数据写入到硬盘 (snapshot & AOF)
- 除了能存储普通的String key之外,还可以存储其他4种data structure
- memcached的不足 :
- 只能存储普通的String key。
- 因为所有的value都是String,当要实现列表的时候,只能把String当作列表来使用。当要删除list里面某个元素时,memcached采用的方法时通过黑名单来隐藏列表里面的元素,从而避免对元素执行读取+更新+写入等操作。
- 无法持久化
- 只能存储普通的String key。
- memcached的优势
- 由于 redis 只使用单核,而 memcached 可以使用多核,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis,虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。
数据结构
- String
- List
- Set
- Hash
- ZSet
持久化
- 快照 (snapshotting) : 将存在于某一时刻的所有数据都写入到硬盘里面。
- 触发条件 :
- 在”指定时间段内有指定数量的写操作执行”这一条件被满足时执行;
- 通过调用两条转储到硬盘 (dump-to-disk) 命令种的任何一条来执行;
- 触发条件 :
- 只追加写入 : 将所有修改了redis的命令都写入到一个只追加文件 (append-only file, AOF) 里面,并设置同步频率
- 从不同步
- 每秒同步一次
- 每写入一个命令就同步一次
过期策略
业界如今有以下3种过期策略。redis采用的是定期删除 + 惰性删除策略。
- 定期删除 : redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查 (如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
- 优点 :
- 频率可控。可以自定义检查的频率,”丰俭由人”。
- 成本可控。有限制地抽取特定数目的元素淘汰,不会干扰到Redis的服务。
- 缺点 : 因为是随机抽取,所以还是会有很多key到时间没有删除。
- 优点 :
- 惰性删除 : 查询某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
- 优点 : 一个兜底的策略,不会让过期的key继续提供value。
- 缺点 : 一直没被查询的话,过期了的key就不会被删除。
- 定时删除 : 用一个定时器来负责监视key,过期则自动删除。
- 优点 : 内存及时释放,
- 缺点 : 十分消耗CPU资源。
为什么不用定时删除策略?因为在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
内存淘汰机制
过期策略其实还不是足够完善。如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置
1
# maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的
淘汰策略 | 详情 |
---|---|
noeviction | 当内存不足以容纳新写入数据时,新写入操作会报错。 ( 应该没人用吧 ) |
allkeys-lru | 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。 ( 推荐使用,目前项目在用这种 ) |
allkeys-random | 当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。 ( 应该也没人用吧,你不删最少使用Key,去随机删 ) |
volatile-lru | 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。 ( 这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐 ) |
volatile-random | 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。 ( 不推荐 ) |
volatile-ttl | 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。 ( 不推荐 ) |
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
Redis常见问题
缓存和数据库双写一致性问题
一致性问题是分布式常见问题,可以再分为最终一致性和强一致性。
数据库和缓存双写,就必然会存在不一致的问题。
- 需要强一致性
- 不能放缓存
- 需要最终一致性
- 采取正确更新策略,先更新数据库,再删缓存。
- 可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
缓存穿透 / 击穿
穿透 即网站遭受到恶意攻击,不断请求数据库中不存在的数据,由于无法击中缓存,因此所有请求都会到达数据库,最终数据库不堪压力而连接异常。 解决方案如下:
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。
- 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
- 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。
布隆过滤器
原理 :
- 每新增一个元素,用若干个hash算法计算出其hashcode,然后将对应的bit array设为1。
- 查询是否存在时,用若干个hash算法计算出存在的元素的hashcode,然后看对应的bit array是否全为1
- 是 : 有可能存在
- 否 : 一定不存在
缓存雪崩
雪崩 即缓存同一时间大面积的失效,这个时候会有大量请求同时到达数据库,导致数据库连接异常。
- 给缓存的失效时间,加上一个随机值,避免集体失效。
- 互斥锁,但该方案吞吐量明显下降。
集群模式
Redis的使用场景
如何使用Redis做分布式锁
实现分布式锁主要利用 redis 中set nx ex的API。
nx
是if not exists
的缩写,当key已经存在了,则存储不成功,返回0。当key不存在时,操作成功,返回1。
ex
是expire time
的缩写,在设这个key value的时候,同时设定过期时间。
下面是一个分布式锁的 Java 实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
private StringRedisTemplate stringRedisTemplate;
public boolean acquirePaymentLock(String memberId, String value, Instant expireDate) {
return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
Object result = connection.execute("set", memberId.getBytes(), value.getBytes(),
"nx".getBytes(),
"ex".getBytes(), expireSecondStr.getBytes()
);
if (result == null) {
logger.error("failed to acquire lock");
throw new PaymentLockException(GENERIC_ERROR, memberId, value);
}
return "OK".equals(new String((byte[]) result));
});
}