redis是一个高性能key-value内存数据库。在日常开发中使用redis最常见的就是当做缓存,基于redis的特殊数据结构和相关特性,redis的应用场景还很多,比如实现分布式锁、延迟消息、消息队列等功能。本文将介绍redis的基本数据类型及其应用场景、redis的过期策略、持久化、集群、以及在日常开发中的使用。
基本数据类型
数据类型 | value | 操作 | 运用场景 |
---|---|---|---|
string | 可以使字符串、整数或浮点数 | 对整个字符串或者字符串的其中一部分进行操作;对整数和浮点数执行自增(increment)或者自减(decrement)操作 | 计数器(浏览数)、分布式全局唯一id |
list | 一个链表,链表上每个节点都包含了一个字符串 | 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或多个元素;根据值查找或者移除元素。 | 消息队列、用户列表 |
set | 包含字符串的无序收集器,并且被包含的每个字符串都是独一无二、各不相同 | 添加、获取、移动单个元素;检查元素是否存在于集合中;计算交集、并集、差集;从集合里面随机获取元素 | 抽奖活动、电商商品筛选 |
hash | 包含键值对的无序散列表 | 添加、获取、删除单个键值对;获取所有的键值对 | 用户信息等发展对象 |
zset | 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排序顺序由分值的大小决定 | 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素 | 排行榜、好友列表 |
过期策略
redis所有的数据结构都可以设置过期时间,时间一到,就会自动删除。如果redis中k有很大数据量的key,redis又是用什么机制保证能够高效的删除过期的key呢。
redis采用定期删除策略+惰性删除的形式来删除过期key。对于设置过期时间的key,redis会单独维护一个字典存放这个key,然后redis默认每10秒扫描过期字典中的key,但并不是扫描所有的key值,而是
- 随机抽选20个key
- 删除其中过期的key
- 如果其中过期key的数量超过1/4,重复步骤1
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除。
淘汰策略
当实际内存超出 redis配置的最大使用内存时,redis 提供了几种可选策略 (maxmemory-policy) 来让用户自己决定该如何腾出新的空间以继续提供读写服务。
noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。
volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。
allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。
持久化
策略 | 存储方式 | 生成方式 |
---|---|---|
快照(RDB) | 快照是全量备份,快照是内存数据的二进制序列化形式,在存储上非常紧凑。 | RDB是通过Redis主进程fork子进程,让子进程执行磁盘 IO 操作来进行 RDB 持久化。RDB记录的数据。 |
AOF日志 | AOF 日志是连续的增量备份。AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。 | AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。AOF记录的是指令。 |
redis4.0采用RDB与AOF混合使用,这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。于是在 redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
使用
缓存
使用redis当缓存是日常开发中最常使用的方式,利用redis的高性能把热点数据和一些稳定不变的数据放入缓存中,减少db的压力,提高系统的性能和接口的响应速度。
读写策略
将数据放入缓存中,就会涉及一个问题,缓存和数据库的一致性。选择什么样的缓存读写策略能够减少缓存与数据库的数据不一致的情况少发生。
Cache Aside(旁路缓存)策略
- 读:
从缓存中读取数据;
如果缓存命中,则直接返回数据;
如果缓存不命中,则从数据库中查询数据;
查询到数据后,将数据写入到缓存中,并且返回给用户。 - 写:
更新数据库;
删除缓存;
写的时候为什么不是更新缓存而是删除缓存,因为更新缓存很容易出现缓存不一致的问题。因为更新数据库和更新缓存并不是一个原子操作,下面举例说明一下。
比如现在需要更新商品表中的单价,初始为20。操作A将单价改为25,将数据库中的单价从20修改25,同时操作B也将单价改为30,将数据库的单价从25修改为30,并且把缓存中的数据修改为30,然后操作A将缓存中的数据修改25。此时缓存中的数据为25,数据库为30出现可缓存不一致的问题。
- A更新数据库单据从20为25;
- B更新数据库从25到30;
- B更新缓存为30;
- A更新缓存为25;
更新之后去删除缓存,并发去更新的数据就不会出现缓存不一致的问题,因为每次更新都是去删除key值,读取的时候都是加载最新的数据。
但是旁路缓存还是会出现缓存不一致的问题,只是出现的几率不高。假如缓存中商品不存在,请求A从数据库中读取到商品单价20,还没有放入缓存的时候,这时候请求B将数据库中商品单价修改为25,然后请求A将单据20放入缓存。只不过这种情况很少出现,因为写入缓存,是比写入数据库快很多的。如果非要保证数据库和缓存的一致性,可以使用分布式锁去实现操作数据库和缓存的原子性,但这样性能会丢失很多,也失去使用缓存的初衷。
- A从数据库中读取单价20;
- B更新数据库从20到25;
- B删除缓存;
- A更新缓存为20;
缓存问题
问题 | 描述 | 解决方案 |
---|---|---|
缓存穿透 | 缓存穿透是指查询一个不存在的数据。会导致每次请求都会到存储层去,失去了缓存的意义。 | 1、布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。2、如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 |
缓存雪崩 | 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。 | 缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 |
缓存击穿 | 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。 | 在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。 |
分布式锁
使用redis作为分布式锁,利用redis的指令setnx(set if not exists)当key设置值不存在时,设置值成功的机制实现。
问题 | 解决方案 |
---|---|
当客户端争取到锁之后挂掉,导致锁无法释放 | 设置key的过期时间 |
setnx 和 expire并不是原子操作有可能还没来得及设置过期时间客户端挂掉 | reids2.8后新的指令和将其合并为一条set key value [ex seconds] [px milliseconds] [nx xx] |
过期时间时长的问题,可能key已经过期,但是内部操作还没执行完成,新的请求又可以取获取锁 | 1、保证业务不会超过这个过期时间 2、遇到这种需要阻塞等待的业务场景推荐使用zk实现分布式锁 |
延迟消息
redis2.8之后推出键空间通知事件
利用redis的key过期通知机制,实现延迟消息,比如30分钟关闭订单、延迟通知等功能。
- 配置key过期事件
修改redis.config文件,开启 notify-keyspace-events Ex 配置,redis推送key过期时间,redis默认情况下会禁用所有通知,所以将notify-keyspace-events ""修改为notify-keyspace-events "Ex"。开启之后redis会以发布/订阅的形式,当key过期时候,会推送key值过期的消息。客户端订阅到指定key之后,可以解析key的所代表的的业务含义,做相关的业务操作。
# Redis can notify Pub/Sub clients about events happening in the key space.
# This feature is documented at http://redis.io/topics/notifications
#
# For instance if keyspace events notification is enabled, and a client
# performs a DEL operation on key "foo" stored in the Database 0, two
# messages will be published via Pub/Sub:
#
# PUBLISH __keyspace@0__:foo del
# PUBLISH __keyevent@0__:del foo
#
# It is possible to select the events that Redis will notify among a set
# of classes. Every class is identified by a single character:
#
# K Keyspace events, published with __keyspace@__ prefix.
# E Keyevent events, published with __keyevent@__ prefix.
# g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
# $ String commands
# l List commands
# s Set commands
# h Hash commands
# z Sorted set commands
# x Expired events (events generated every time a key expires)
# e Evicted events (events generated when a key is evicted for maxmemory)
# A Alias for g$lshzxe, so that the "AKE" string means all the events.
#
# The "notify-keyspace-events" takes as argument a string that is composed
# of zero or multiple characters. The empty string means that notifications
# are disabled.
#
# Example: to enable list and generic events, from the point of view of the
# event name, use:
#
# notify-keyspace-events Elg
#
# Example 2: to get the stream of the expired keys subscribing to channel
# name __keyevent@0__:expired use:
#
# notify-keyspace-events Ex
#
# By default all notifications are disabled because most users don't need
# this feature and the feature has some overhead. Note that if you don't
# specify at least one of K or E, no events will be delivered.
notify-keyspace-events "Ex"
- 代码示例
引入jedis
redis.clients
jedis
2.9.0
事件监听
public class KeyExpireListener extends JedisPubSub {
@Override
public void onPMessage(String pattern, String channel, String message) {
System.out.println("expired_event key : "+message);
// 拿到定义的key之后做具体的业务
}
}
事件订阅
public class RedisSubscribe {
private Jedis jedis;
private JedisPubSub jedisPubSub;
private String topic;
public RedisSubscribe(Jedis jedis,JedisPubSub jedisPubSub,String topic){
this.jedis = jedis;
this.jedisPubSub = jedisPubSub;
this.topic = topic;
}
public void subscribe(){
//订阅会阻塞线程,新开线程监听事件
new Thread(()->{
jedis.psubscribe(jedisPubSub, topic);
}).start();
}
}
测试
public static void main(String[] args) throws Exception{
Jedis jedis = new Jedis("127.0.0.1",6379);
String topic = "__keyevent@0__:expired";
RedisSubscribe redisSubscribe = new RedisSubscribe(new Jedis("127.0.0.1",6379), new KeyExpireListener(), topic);
redisSubscribe.subscribe();
String orderId = "123";
jedis.set("order:close:"+orderId, orderId);
jedis.expire("order:close:"+orderId, 5);
}
虽然这种方式可以实现业务功能,但是不能保证消息的可靠性,推荐还是使用专业的消息队列实现。
总结
以上是关于redis的一些总结,平时使用还是需要多学习内部原理。