解决高并发问题的其中一项措施是使用缓存,而通常的技术选型就是redis。
用户访问网站时,为了避免每次都到持久层(如mysql)中获取数据,可以先到缓存(如Redis)中获取;如果缓存中获取不到,才到数据库中获取,同时将获取到的数据缓存到redis中。加缓存的目的是让用户尽可能少的访问数据库,尽可能多的访问缓存数据,从而提高网站的响应速度,保证网站的高并发,保护持久层数据的安全,同时提升用户的体验。
有个黑帽子,一直使用订单id=-1的请求参数访问你的网站,会怎么样?
缓存穿透
比如数据中不存在id=-1的订单数据,如果请求查询这条数据,则缓存中查不到,会将请求打到下层的数据库上,这就是缓存穿透;
查询缓存中不存在的数据会导致缓存穿透。需要注意的是,低频的缓存穿透是不可避免的,但是需要避免高频的缓存穿透。
如果有人恶意并发访问数据库中不存在数据,就可能会导致数据库因扛不住大的并发而引起系统瘫痪!
解决方案一:缓存空对象
请求到redis,当redis没有命中该数据时,请求会到达mysql;如果mysql也不存在该数据时,则缓存一个空对象到redis中。这样就可以解决缓存穿透问题。
public String getOrderInfo(String orderId){
// 查询缓存
String orderInfoStr = redisClient.get(orderId);
// 缓存不存在,查询数据库
if(orderInfoStr == null){
OrderInfo orderInfo = orderMapper.selectByOrderId(orderId)
if(orderInfo != null){
// 数据库存在,则正常缓存
orderInfoStr = JSON.toJSONString(orderInfo);
redisClient.set(orderId,orderInfoStr,2L * 60 * 60 * 1000,TimeUnit.MILLISECONDS);
}else{
// 数据库存在,则短时间缓存空值
orderInfoStr = "";
redisClient.set(orderId,orderInfoStr,30 * 60 * 1000,TimeUnit.MILLISECONDS);
}
}
return orderInfoStr;
}
问题来了,当数据库中真的插入了该条数据,请求过来后只能拿到缓存中的空对象,该如何解决呢?
我们可以针对这种数据设置一个较短的过期时间,保证数据库和redis的弱一致性,就可以在一定程度上解决这个问题。
缓存空对象就没有其他问题了吗?no!!!
上面那个黑帽子使用订单id=-1的参数高并发请求你的系统,发现你的系统依然稳如狗,于是简单的改变了一下策略,随机生成负数订单id后高并发请求你的系统,你的系统会怎样?
这时,你的redis中会缓存大量的值为空的key,这会导致大量的内存占用,同时Redis有LRU或LFU的内存淘汰策略,可能会将缓存中有价值的数据淘汰掉,真正的用户请求过来会将请求打到数据库上。
所以,这种方式的缺点是:
- 可能会缓存很多值为空的key,占用内存空间。同时Redis有LRU或LFU的内存淘汰策略,可能会将缓存中有价值的数据淘汰掉。
- 对空值设置了时间,可能会导致数据库和redis中在某个时间段的数据不一致。
解决方案二:布隆过滤器
没有什么问题是加一层不能解决的!如果有,就再加一层! 为了防止缓存穿透,可以将数据库中存在的id提前存放在一个List数组,后续,请求到达controller层,可以到List数组中查询这个id是否存在,如果存在则将请求往下发送,如果不存在,则直接返回。
懂门路的朋友马上就看出破绽了,如果数据库中的数据量很大,这个List数据会占用很大的空间,同时查询的效率也会降低,有没有解决办法呢?当然有!使用Bloom Filter。
Bloom Filter是一个占用空间很小、效率很高的随机数据结构,它由一个bit数组和一组Hash算法构成。可用于判断一个元素是否在一个集合中,查询效率很高,内节省内存空间。
Bloom Filter在这里留个坑,以后有机会详细再说。
需要注意的是,Bloom Filter存在哈希碰撞问题,有一定的错误率;但是有一点可以肯定的是:**Bloom Filter说这条数据存在,这条数据不一定存在;但是Bloom Filter说这条数据不存在,这条数据一定不存在。**要使用好Bloom Filter过滤器,要从bit数组和Hash算法的角度做优化。
缓存击穿
缓存击穿是缓存穿透的特殊表现之一。一般的公司没有这样的业务,没有这样一条非常热的数据能够导致数据库崩溃,所以不需要解决。
当某个数据被高并发访问时,如果这个数据redis的key的突然失效,会导致这些请求同一时间打到数据库,数据库扛不住就会导致系统瘫痪。比如微博上突然爆出某个明星的出轨、结婚等消息,大家同时都到微博上搜这个明星的信息,这时如果这个热点key过期,就会导致微博挂掉。志玲姐姐结婚了,微博的程序猿已经够伤心的了,还要加班修复系统。
对于一般的公司来说,不会存在一条这样的热点数据,当这条数据失效的一瞬间将请求打到数据库从而导致系统崩溃的,所以也不用过度担心这个问题。
解决方案一:热点数据不过期
最简单的方式就是,缓存这些热点数据的时候,不设置过期时间,这样就不用担心这个问题了。
新的问题又来了,当数据库中这些已有的数据发生变化了,缓存没有变,导致数据不一致,怎么办?
这就需要一种方式来保证数据库和redis中的数据的一致性。数据库和redis的强一致是很难做到的,但是弱一致性还是可以保证的。方式有很多,比如数据库中数据发生变化时,可以同步更新redis中的数据;可以通过相关的中间件监控mysql的binlog日志并更新redis等。
解决方案二:分布式锁
当热点key过期,允许这个热点key在redis中查询不到数据时将其中一个请求打到达数据库,但不是并发打到数据库,然后将数据缓存到redis,后面的请求就可以到redis中查询到数据了。
比如100W个人同时到微博查看志林解决结婚的这条信息,这时正好redis中的这个热点key过期,假设微博后端部署了10台服务器,如何保证这100w人中只有一个人的请求在查询不到缓存数据将请求打到数据库,并将查询到的数据缓存到redis,然后其他999999个请求都从缓存中拿数据呢?
显然JDK提供的java同步锁synchronized是不能实现的,因为这种方式只能在一个JVM中生效;分布式部署的多台机器就需要使用分布式锁。
分布式锁其实就是在外部存储空间一个标签,当多台机器同时需要访问某个相同资源,就需要去竞争这把锁,谁竞争到锁谁就有权利访问这个资源,其他的机器就需要等待,当这台机器访问完成后就释放锁,其他的机器继续竞争锁,以此类推。
分布式锁的实现也很多,最常用的就是zookeeper和redis。
缓存雪崩
缓存雪崩也是缓存穿透的特殊表现之一。
上面说的缓存击穿是一个热点key的失效,而缓存雪崩是多个热点key同时失效。
一般出现缓存雪崩的原因是:
- 缓存过期的时间比较一致,某一时刻key大面积失效。解决办法:将缓存时间设置成一个随机数。
- redis挂了,或因为网络抖动访问不了redis了。解决办法:使用redis集群。
解决方案一:数据预热,缓存时间随机
这种方式比较简单,可以专门做一个mysql和redis数据的同步服务,项目启动时,使用同步服务将mysql中的基础数据比如商户、门店等相关信息加载到redis中,并设置随机的失效时间;同时再每隔一个固定的时间(比如6h)同步一次。
如果要求修改mysql后,及时同步到redis,就需要有其他的措施保障了。
解决方案二:redis集群
为了防止redis挂掉,就要使用redis集群做高可用,可以将数据进行分片,将同样的数据分布到多台机器上;当集群中的一台或几台机器宕机,也依然能保障redis是可用的。