zhangll00 2019-10-27
一般的缓存系统,都是按照key去查询缓存,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。(只对于登录的恶意用户群体)
如何避免:
1:网上有好多说法是分段自增设置缓存的失效时间,避免在同一时间段造成大量的缓存失效。
我想说的是系统默认永久,你去动它干什么?而且在需要缓存的业务中,新增、修改、删除都有对应的缓存策略,你动它干什么?甚至内存,哪些该舍弃,哪些该保留,redis都有自己的一套方案。如果考虑Redis机器宕机,可考虑主从复制方案。
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
如何避免:
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
首先不存在服务器重启,大量的缓存更新,从业务上讲,有查询才有缓存。其次,redis有持久化机制,默认是RDB,当然设置成AOF更好。我觉得没必要,不存在,去查一遍并更新就好了嘛。
综上,我们需要做到是先查缓存,没有就去查数据库再更新到缓存,并且保证只能有一个线程执行查询数据库并更新到缓存这个任务就可以了。至于没有查询的业务,你加载到缓存干什么?
于是redis分布式锁就诞生,原理是第一个请求的资源加锁,执行查询DB并更新缓存的操作。其他资源看到有锁就等待,然后执行查询。于是乎,需要三个方法,分别是:加锁、解锁、轮询。
使用redis命令 set key value NX EX max-lock-time 实现加锁
Jedis jedis = new Jedis("127.0.0.1", 6379); private static final String SUCCESS = "OK"; /** * 加锁操作 * @param key 锁标识 * @param value 客户端标识 * @param timeOut 过期时间 */ public Boolean lock(String key,String value,Long timeOut){ String var1 = jedis.set(key,value,"NX","EX",timeOut); if(LOCK_SUCCESS.equals(var1)){ return true; } return false; }
解读:
加锁操作:jedis.set(key,value,"NX","EX",timeOut)【保证加锁的原子操作】
key就是redis的key值作为锁的标识,value在这里作为客户端的标识,只有key-value都比配才有删除锁的权利【保证安全性】
通过timeOut设置过期时间保证不会出现死锁【避免死锁】
NX,EX什么意思?
NX:只有这个key不存才的时候才会进行操作,if not exists;
EX:设置key的过期时间为秒,具体时间由第5个参数决定
使用redis命令 EVAL 实现解锁
Jedis jedis = new Jedis("127.0.0.1", 6379); private static final Long UNLOCK_SUCCESS = 1L; /** * 解锁操作 * @param key 锁标识 * @param value 客户端标识 * @return */ public static Boolean unLock(String key,String value){ String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end"; Object var2 = jedis.eval(luaScript,Collections.singletonList(key), Collections.singletonList(value)); if (UNLOCK_SUCCESS == var2) { return true; } return false; }
解读:
luaScript 这个字符串是个lua脚本,代表的意思是如果根据key拿到的value跟传入的value相同就执行del,否则就返回0【保证安全性】
jedis.eval(String,list,list);这个命令就是去执行lua脚本,KEYS的集合就是第二个参数,ARGV的集合就是第三参数【保证解锁的原子操作】
试想一下如果在业务中去拿锁如果没有拿到是应该阻塞着一直等待还是直接返回,这个问题其实可以写一个重试机制,根据重试次数和重试时间做一个循环去拿锁,当然这个重试的次数和时间设多少合适,是需要根据自身业务去衡量的。
/** * 重试机制 * @param key 锁标识 * @param value 客户端标识 * @param timeOut 过期时间 * @param retry 重试次数 * @param sleepTime 重试间隔时间 * @return */ public Boolean lockRetry(String key,String value,Long timeOut,Integer retry,Long sleepTime){ Boolean flag = false; try { for (int i=0;i<retry;i++){ flag = lock(key,value,timeOut); if(flag){ break; } Thread.sleep(sleepTime); } }catch (Exception e){ e.printStackTrace(); } return flag; }