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分布式锁的实现
可靠性
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
加锁的简单实现方法:每个客户端或者说每一个对象对于同一个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操作。