1.redis的简单介绍

   redis是一种高性能的Key-Value类型的内存数据库,之所以说性能非常好,是因为redis是存放在内存中的,存取都是不用进行io磁盘操作,所以效率十分高。

2.为什么会使用redis这种数据结构

   其一:就是性能好,可以节约大量的时间。其二:在高并发的情况下,如果所有的请求(一般查询偏多)都直接请求数据库,会导致数据库连接异常。如果在这种情况下,先请求redis缓存,即可有效缓解这种问题。

3.redis可支持的数据类型及使用场景

redis除了性能和高并发还能支持多种数据类型,包括:list,set,hash,sorted set, hash共五中数据接口。

4.redis单线程效率为何那么高

  •   纯内存操作,没有IO磁盘操作,因此效率很高。
  •   单线程操作,避免了频繁的上下文切换,从一定角度来看也是提高了效率。
  •   采用了非阻塞I/O多路复用机制

5.redis数据持久化

Redis 提供了多种不同级别的持久化方式:

  • RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。
  • AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。
  • Redis 还可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。
  • 你甚至可以关闭持久化功能,让数据只在服务器运行时存在。

了解 RDB 持久化和 AOF 持久化之间的异同是非常重要的, 以下几个小节将详细地介绍这这两种持久化功能, 并对它们的相同和不同之处进行说明。

RDB:RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份。虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。所以如果不允许丢失数据的RDB不合适。

AOF:使用 AOF 持久化会让 Redis 变得非常耐久。AOF 文件有序地保存了对数据库执行的所有写入操作。对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。并且效率比RDB低。

 究竟选择哪一种方法:

 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快, 除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的 bug 。

6.redis分布式锁的实现

可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

 加锁的简单实现方法:每个客户端或者说每一个对象对于同一个key都只能执行一次set,保证了加锁的机制。等操作完成及时解锁。

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

解锁的简单实现  :根据key对应的value,进行删除锁。

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

  应用场景:使用redis实现分布式锁,达到分布式部署场景下用户借款串行处理(防止产生多笔重复借款)。

7.redis的过期策略以及内存淘汰机制

redis采用的是定期删除和惰性删除,而还有一种叫定时删除,

为什么不用定时删除策略?

定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.

定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。

如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。

 内存淘汰机制:

在redis.conf中有一行配置

一共6种方式:

1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。

2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种。

3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。

4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐

5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐

6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐

8.redis所带来的问题以及解决方案

(一)缓存和数据库双写一致性问题

     具体问题是指在更新数据库时,因为有缓存存在,会导致缓存和数据库中数据不一致的问题。

一般解决方案有以下几种:

(1)先更新数据库,在更新缓存。

多线程情况下:

  1)线程A更新了数据库
  2)线程B更新了数据库
  3)线程B更新了缓存
  4)线程A更新了缓存

   明显应该A先更新缓存,可B却先更新了缓存,导致了脏数据。并且如果写操作比较多,缓存还没有被读就一直在更新,就会浪费这次操作,影响性能。

   因此:一般都采用这种方式解决一致性问题。

(2)先删除缓存,在更新数据库。

    多线程的案例:

   1)请求A进行写操作,删除缓存
   2)请求B查询发现缓存不存在
   3)请求B去数据库查询得到旧值
   4)请求B将旧值写入缓存
   5)请求A将新值写入数据库

   导致了缓存和数据库不一致。如果没有key过期作废的机制,这个脏数据就会一直存在。

(3)先更新数据库,在删除缓存。

   发生这种情况的案例:

     1)缓存刚好失效
     2)请求A查询数据库,得一个旧值
     3)请求B将新值写入数据库
     4)请求B删除缓存
     5)请求A将查到的旧值写入缓存
 如果发生上述情况,确实是会发生脏数据。但是概率很小。因为步骤5)一般都会在步骤3)前执行。因为读操作远快于写操作,所以这种策略比较常用。

(二)缓存雪崩问题

可能是并发大量请求导致缓存失效(查不到值),即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。

解决方案:

(1)给缓存的失效时间,加上一个随机值,避免集体失效。

(2)使用互斥锁,但是该方案吞吐量明显下降了。

(3)双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点

  • I 从缓存A读数据库,有则直接返回

  • II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。

  • III 更新线程同时更新缓存A和缓存B。

(三)缓存击穿问题

     缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。

解决方案:

(1)利用互斥锁,缓存中不存在数据的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试

(2)采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。

(3)提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。

(四)缓存的并发竞争key问题

多个子系统同时set同一个值,导致竞争key的问题。

解决方案:

(1)如果对这个key操作,不要求顺序

这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。

(2)如果对这个key操作,要求顺序

系统A key 1 {valueA  3:00}

系统B key 1 {valueB  3:05}

系统C key 1 {valueC  3:10}

当系统抢到分布式锁的时候,还要判断时间戳,如果小与缓存中的时间戳,则不去set操作。

12-21 07:27