Redis introduction

Hello Redis

Posted by Vincent on August 27, 2020

什么是Redis

  1. 内存数据库
  2. 非关系型数据库 (数据库不需要被预定义)
  3. 有5种独特的数据类型
  4. 特性
    • 可持久化
    • 可复制 (扩展读性能 & 提供故障转移)
    • 可客户端分片 (扩展写性能)

Redis的好处

为什么 redis 单线程模型也能效率这么高?

  1. 内存操作
  2. 核心是基于非阻塞的 IO 多路复用机制 (像Windows一样的事件处理机制)
  3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
    • Redis是基于内存的操作,CPU不是Redis的瓶颈 ,Redis的瓶颈最有可能是机器内存的大小或者网络带宽 (也就是就算采用多线程去榨干CPU资源,还是得等网络带宽)。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。
    • 不过无法发挥多核CPU性能,但是可以通过在单机开多个Redis实例来完善;
  4. 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
  5. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

对比memcached

  • 相同点 :
    • 都可用于存储key-value mapping
    • 性能也相差无几
  • Redis的优势 :
    • 能够自动以两种不同的方式将数据写入到硬盘 (snapshot & AOF)
    • 除了能存储普通的String key之外,还可以存储其他4种data structure
  • memcached的不足 :
    • 只能存储普通的String key。
      • 因为所有的value都是String,当要实现列表的时候,只能把String当作列表来使用。当要删除list里面某个元素时,memcached采用的方法时通过黑名单来隐藏列表里面的元素,从而避免对元素执行读取+更新+写入等操作。
    • 无法持久化
  • memcached的优势
    • 由于 redis 只使用单核,而 memcached 可以使用多核,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis,虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。

数据结构

  1. String image-20200901095423867
  2. List image-20200901094745076
  3. Set image-20200901094732166
  4. Hash image-20200901094719409
  5. ZSet image-20200901094636092

持久化

  1. 快照 (snapshotting) : 将存在于某一时刻的所有数据都写入到硬盘里面。
    1. 触发条件 :
      1. 在”指定时间段内有指定数量的写操作执行”这一条件被满足时执行;
      2. 通过调用两条转储到硬盘 (dump-to-disk) 命令种的任何一条来执行;
  2. 只追加写入 : 将所有修改了redis的命令都写入到一个只追加文件 (append-only file, AOF) 里面,并设置同步频率
    1. 从不同步
    2. 每秒同步一次
    3. 每写入一个命令就同步一次

过期策略

业界如今有以下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。

nxif not exists的缩写,当key已经存在了,则存储不成功,返回0。当key不存在时,操作成功,返回1。

exexpire 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));
    });
}