Redis 的key设计技巧
1.1 原主键列进行查询
比照关系数据库的设计:
1): 把表名转换为key前缀.
2): 第2段放置用于区分区key的字段–对应mysql中的主键的列名,如userid
3): 第3段放置主键值,如2,3,4…., a , b ,c
4): 第4段,写要存储的列名
对该表设置redis的key如下设计:
1.2原非主键列进行查询
两步进行:
1)先根据非主键列设置一个key,存储的是key的值
2)然后根据查到的key再到第一步中的key中取查询具体的值.
即:
在关系型数据中,除主键外,还有可能其他列也步骤查询,
如上表中, username 也是极频繁查询的,往往这种列也是加了索引的.
转换到k-v数据中,则也要相应的生成一条按照该列为主的key-value
Set user:username:lurenjia:uid 1
这样,我们可以根据username:lurenjia:uid ,查出userid=1,
再查user:1:password/email …
完成了根据用户名来查询用户信息
Redis 使用中的常见问题
2.1 缓存穿透(缓存击穿)
2.1.1什么是缓存穿透?
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
缓存穿透简单说来就是缓存根本没用上了,缓存中不存在然后去DB查,然后DB查没有,然后不缓存,然后来了缓存中不存在又去DB查.
2.1.2 如何避免
1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
2: 使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db, 而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法.
代码如下:
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。在redis2.6.1之前版本未实现setnx的过期时间,所以这里给出两种版本代码参考
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
8.2 缓存雪崩
8.2.1 什么是缓存雪崩?
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
即:
如果是数据从缓存中查询,如果没有则从mysql中查询,然后缓存到缓存中.这种模式在并发量并非太高或数据操作效率很高的情况下基本没有什么问题。
但是如果if(缓存失效 && 恰好遇到并发量很高 && 数据库操作时间长) then?
1. 缓存失效
2. 第一个进程去数据库获取新数据,假如包括SQL+程序逻辑耗时5S
3. 这5S内,第二个、第三个…第N个都只是获取到已失效的缓存,于是也都连接数据库…
4. 结果显而易见,数据库锁表 -> 数据库累计大量进程 -> 直至数据库挂掉!
8.2.2 如何避免?
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
3:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期.
4: 添加一个标记,类似文件锁,用于判断此时程序是否正在更新缓存。
若是,则直接返回旧缓存(标记有设置失效时间,避免由于程序错误导致标记未删除而引起的缓存不更新问题)
若否,则设置一个标记,然后进行数据获取及缓存更新,最后删除标记。