Redis是一个高效的NoSQL数据库,采用Key-Value保存数据,一般作为高速分布式缓存使用。
Redis Key的设计技巧
Redis作为高速缓存,要能通过Key快速的查到需要的数据,一般作为数据库的缓存,所以Redis的Key设计可参考数据库表。
以user表为例,数据库设计如下:
user_id | user_name | password | |
---|---|---|---|
1 | zhangsan | secret1 | [email protected] |
2 | lisi | secret2 | [email protected] |
Key是设计建议如下:
- Key字段通过冒号分割
- 表名作为Key前缀
- 表主键的字段名作为第二段
- 表主键的字段值作为第三段
- 要查询的字段作为第四段
现在要通过user_id
(1)快速查询用户user_name
(zhangsan)
则缓存数据设计如下:
set user:user_id:1:username zhangsan
如果user_name
字段是索引字段,且需要频繁通过user_name查询用户信息,则可以把查询关键字段。
则缓存数据设计如下:
set user:user_name:zhangsan:user_id 1
然后在通过user_id查询其他用户信息。
当然,Key的设计还是需要根据业务需要灵活处理,这里只是给出一个一般思路。比如以关键字+日期为Key进行日期相关的统计。
Redis数据类型
基本类型
Redis基础的数据类型包括String、List、Hash(Map)、Set、SortedSet 5种。
String
String是最基本的数据类型,它不但可以保存字符串,也用来保存int,long,float等单值数据,也可以作为计数器。很多项目也会把对象通过JSON序列化后保存为String,使用的时候在反序列化回来。如果使用这种方式,需要通过代码注释或文档对字段进行详细的说明,避免日后自己或他人维护时“相见不相识”。
场景使用场景包括:
- 普通字符串缓存
- int、long、float等其他基本类型
- 计数器(INCR,INCRBY,DECR,DECRBY等)
- 分布式锁(SETNX
SET if Not eXists
) - Bitmap(SETBIT,GETBIT,BIT*),可用于海量数据统计或Bloom过滤器等。
List
List是有序列表,使用方式和Java中的List类似,支持列表头尾插入(LPUSH,RPUSH),头尾弹出(LPOP,RPOP),阻塞式弹出(BLPOP,BRPOP),按范围获取(LRANGE)等丰富的操作。
常见使用场景包括:
- 分布式消息队列
- 缓存门户首页热点文章、热点商品
- 分页获取数据
Hash
Hash类型类似Java中的HashMap,保存Key-Value键值对。可以直接把一个数据库表的行映射到缓存,也可以保存无嵌套类型的POJO。
Set
Set是无序不重复的集合,使用方式和Java中的Set类似,可以做高效的Set间的交、并、差集等操作(SINTER,SUNION,SDIFF)。
Sorted Set
有序集合,类似Java中的LinkedHashSet,但不是根据插入顺利来排序的,而是Set的每个元素可以关联一个double类型的分值(score)用于排序。基于这个特性,Sorted Set的典型场景包括:
- 排行榜(热搜):可以实现门户首页按点击量、按时间、按点赞数等排序场景。
- 带权重的消息队列:根据消息的重要程度,设置不同的score。
高级类型
Bitmap
通过基本类型中String的BIT操作方法,实现位操作,包括SETBIT,GETBIT,BITCOUNT,BITOP等,可用于海量数据统计或Bloom过滤器等。比如统计日活用户量,每Bit表示一个用户,index为用户ID(假设ID为int型),初始化全部为0,用户登录后对应Bit设置为1,通过统计1的个数即可统计用户日活量。
HyperLogLog
这个待研究!!!
Pipleline
这个也待研究!!!
发布订阅模式(Pub/Sub)
Redis 发布订阅(Pub/Sub)是一种消息通信模式:发送者(Pub)发送消息,订阅者(Sub)接收消息。客户端可以订阅任意数量的频道。
订阅者监听消息:
SUBSCRIBE channel_name
发送者发送消息:
PUBLISH channel_name "Message"
Lua脚本
Redis 脚本使用 Lua 解释器来执行脚本。 Redis 2.6 版本通过内嵌支持 Lua 环境。执行脚本的常用命令为EVAL。
Eval 命令的基本语法如下:
EVAL script numkeys key [key ...] arg [arg ...]
脚本示例:
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
Redis事务
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中
一个事务从开始到执行会经历以下三个阶段:
- 开始事务
- 命令入队
- 执行事务
事务命令示例:
MULTI #开始事务
SET book-name "Mastering C++ in 21 days"
GET book-name
SADD tag "C++" "Programming" "Mastering Series"
SMEMBERS tag
EXEC #执行事务
缓存淘汰策略
为了保证高性能,缓存都保存在内存中,当内存满了之后,需要通过适当的策略淘汰老数据,以便腾出空间存储新数据。数据的淘汰策略,典型的包括FIFO(先进先出,淘汰最老数据),LRU(淘汰最近最少使用),LFU(淘汰最近使用频率最低的)。
FIFO很简单就不展开了,主要说下LRU和LFU的区别,详细区别参考这里。
- LRU(Least Recently Used),首先淘汰最长时间未被使用的数据。实现方法是每次访问数据后把数据移到队头,删除时从队尾开始删除。
- LFU(Least Frequently Used),首先淘汰一定时期内被访问次数最少的数据。实现方法是记录数据在一定时段内的访问评率,删除访问频率最低的数据。此算法需要额外维护每个数据的访问量,并排序,实现比较复杂。
Java的LinkedHashMap已经实现了LRU算法,具体实现请查看JDK源码,使用方法请仔细阅读LinkedHashMap以下两个方法的JavaDoc(我贴出来了,注释有点多,有删减)。
/**
* Constructs an empty LinkedHashMap instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - true for
* access-order, false for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
/**
* Returns true if this map should remove its eldest entry.
*
* This implementation merely returns false (so that this
* map acts like a normal map - the eldest element is never removed).
*
* @param eldest The least recently inserted entry in the map, or if
* this is an access-ordered map, the least recently accessed
* entry.
* @return true if the eldest entry should be removed
* from the map; false if it should be retained.
*/
protected boolean removeEldestEntry(Map.Entry eldest) {
return false;
}
Redis提供的淘汰策略包括如下几种:
- noeviction(内存满后不主动回收,无法写入新数据)
- allkeys-lru(最近最少使用的Key)
- allkeys-random(随机回收)
- volatile-lru(过期集合中最近最少使用的Key)
- volatile-random(过期随机回收)
- volatile-ttl(过期最短存活)
Redis部署模式
主备:哨兵、主备、主备链
集群
Redis vs Memcached
常见性能问题
缓存穿透
缓存穿透是指去获取一个Redis和DB都不存在的数据,由于Redis中不存在,导致流量透传到DB,而DB中无相关数据,查询后不会缓存结果到Redis。如果大量此类查询,会给数据库带来性能风险,此问题可被攻击者利用。
避免方法:
- 对于DB中查询不到的数据,也在Redis中进行短期缓存,避免反复查询DB。
- 使用互斥锁(mutex key):到缓存没命中时,不是立即去查询DB,而是先获取一个互斥锁(SETNX命令),获取到锁成功后再去查询DB。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置超时,防止del失败时死锁
if (redis.setnx(key\_mutex, 1, 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire\_secs);
redis.del(key\_mutex);
} else { //没获取到锁,表明其他线程获取了,等待一段时间后重试查询缓存
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
以上代码参考自Redis 的key设计技巧&&缓存问题
缓存击穿
缓存击穿是指在某个热点Key过期的时候,客户端产生大量的情况,导致请求击穿缓存直接到达DB,给DB带来巨大压力,避免方法请参考上述缓存穿透的互斥锁。
缓存雪崩
缓存雪崩是指缓存服务器重启时或者大量缓存在短时间内集中过期时,恰好此时大量客户端执行并发操作,缓存命中失败导致给DB带来巨大压力。
避免方法:
- 查询缓存失败后,查询数据库的代码先加锁再查询数据库,或者队列执行,对于每个Key,每个进程同时只允许一个线程访问数据库,减轻数据库压力。
- 参考上述缓存穿透的互斥锁
- 给每个Key的过期时间后面加个随机值,确保缓存不会在同一时刻大面积失效。
- 设置热点数据永不过期,数据更新后,主动刷新缓存。
数据持久化机制
RDB、AOF
RDB:Fork子进程,新数据COW模式
AOF:同步策略:定时,数据量
集群同步机制
通过RDB完成基准数据同步,再通过AOF进行增量数据同步
缓存和数据库数据一致性
Keys和Scan方法
LRU vs LFU
https://www.jianshu.com/p/02b...
https://blog.csdn.net/a319204...
https://www.cnblogs.com/sddai...