1. 缓存使用的场景
1. DB缓存,减轻服务器压力
通常,我们的数据都存储在数据库中,应用程序直接操作数据库。当访问量增大,数据库压力过大时,可以采取的方案有:读写分离、分库分表。但是如果访问量达到10万甚至百万即,就需要引入缓存了(因为关系型数据库数据在硬盘,频繁IO,日志刷盘根本顶不住)。将已经访问的数据存储起来,当再次访问时,先找缓存,命中就返回数据,没有命中再去数据库查询,并回填缓存。
2. 提高系统响应
关系型数据库数据都是存储在磁盘中的,直接访问数据库,需要磁盘频繁的与内存做交换,几万条数据,可以分库分表,读写分离,让每个服务器都少IO一点。但是百万级别的,用分库分表,读写分离的方案,需要的服务器就太多了。如果服务器少了,大量的瞬间访问时(高并发),服务器就会因为频繁IO而无法正常响应(而且Mysql的InnoDB引擎是有行锁和表锁的)。此时就可以将数据缓存在内存中,内存天然支持高并发访问,可以瞬间处理大量请求。而且redis的工作线程是单线程的,没有锁,qps达到了11万/s读操作,8万/s写操作。
3. Session分离
传统的session由服务器自己管理和维护,集群或分布式环境下不同服务器管理的是各种的session,为了session一致性,只能在各个服务器之间,通过网络和io进行session复制,及其消耗系统性能,而且不能保证同步(实时性,复制需要时间)。而有了缓存,就可以将登录成功后的session存放在缓存中,这样多个服务器就可以共享session信息
4. 分布式锁
不单单是线程间的并发。多个进程(JVM)并发时(比如两个服务器上的Tomcat),也会产生同步问题,需要控制时序性,此时可以使用分布式锁(Redis实现sexNX,后面都会详细讲)
5. 乐观锁
同步锁和数据库中的行锁、表锁都是悲观锁,和无锁相比,性能和响应性比较差,因此,高性能、高响应(秒杀),都会采用乐观锁(没有锁,通过CAS算法,根据比对数据版本号从而进行并发控制,同一时刻,只有一个人能成功),Redis可以实现乐观锁watch+incr
2. 缓存的概念和分类
最早,缓存是硬件的概念,CPU上的一种高速存储器(CPU 二级缓存等等),先于内存与CPU交换数据,速度非常快,是觉得内存处理速度跟不上CPU而采用的一种解决方案。
而现在,互联网行业高速发展,缓存的概念,泛指存储在计算机上的原始数据的复杂集,便于快速访问,让系统快速响应的关键技术之一。
我们使用的缓存,比如redis,就是因为不满足磁盘IO的速度,而将内存作为缓存,只不过,redis会用各种数据结构规划这块内存空间,用各种模型和算法,让这块空间得到充分的利用,从而达到非常快的响应速度。
大型网站中,从浏览器到网络,到应用服务器,到数据库,各个层面都可以运用缓存技术,提升系统性能和用户体验。
常见缓存分类
1. 客户端缓存
传统互联网:页面缓存和浏览器缓存
移动互联网:APP缓存
2. 页面缓存
页面自身对某些元素或全部元素进行存储,保存成文件,比如html5的Cookie、WebStorage(SessionStorage和LocalStorage)、WebSql、IndexDB、Application Cache等等
3. 浏览器缓存
客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求的资源”的副本,就可以直接从浏览器缓存获取,分为强制缓存(直接使用浏览器的缓存数据)和协商缓存(服务器资源未修改,使用浏览器的缓存(304),反之,使用服务器的资源(200))
4. APP缓存
原生APP中,会把数据缓存到内存、文件或本地数据库(SQLite)中,比如图片文件。想想自己有没有玩手机游戏,打开图片浏览器一看,好多图标类的文件。
5. 网络端缓存
通过代理的方式响应客户端请求,对重复的请求返回缓存中的数据资源。
6. Web代理缓存
缓存原生服务器静态资源(样式、图片等),常见的反向代理服务器有Nginx
7. 边缘缓存
典型的就是CDN(内容分发网络),通过部署再各地的边缘服务器,让用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率,CDN关键技术主要有内容存储和分发技术,目前的公有云服务商一般都会提供CDN服务
8. 服务端缓存:服务端缓存是整个缓存体系的核心,包括数据库级缓存、平台级缓存和应用级缓存。
1. 数据库级缓存
用于存储和管理数据,MySQL在Server层使用查询缓存机制,将查询后的数据缓存起来(把对应的sql语句和结果缓存),使用的是K-V结构(Key是select的hash值,Vlaue是查询结果)。InnoDB存储引擎中的buffer-pool,就是用于缓存InnoDB索引和数据块的。
2. 平台级缓存
指的是带有缓存特性的应用框架,例如GuavaCache(很重要的一个缓存,以后会专门写文章介绍它)、EhCache(二级缓存,可存硬盘)、OSCache(页面缓存)等,部署再应用服务器上,又称服务器本地缓存
3. 应用级缓存
使用具有缓存功能的中间件(Redis、Memcached、EVCache(国外用的多,AWS等公司)、Tair(国内,阿里,美图等在用)等),采用K-V形式存储,可以利用集群支持高可用、高性能、高并发、高扩展,并可以实现分布式缓存
3. 缓存的优缺点
优点
1. 提升用户体验
用户使用时,一个系统的高响应能力,会让用户体验十分良好
2. 减轻服务器压力
客户端缓存、网络端缓存可以减轻应用服务器压力,服务端缓存可以减轻数据库服务器的压力
3. 提升系统性能(响应时间、延迟、吞吐量、并发能力和资源利用率等)
有效缩短系统响应时间,减少网络传输时间和应用延迟,提高系统吞吐量,增加系统并发能力,提高数据库资源利用率
缺点(都有了成熟的解决方案)
1. 额外的硬件
缓存是用空间换时间的技术,需要额外的磁盘空间和内存空间来存储数据。对于搭建缓存服务器集群需要的资源就更多了,而目前,云计算厂商(阿里云,百度云,腾讯云)提供的云服务器的缓存服务,可以节省大量资源开销。总体来看,利大于弊。
2. 高并发缓存失效的代价过高
能用到缓存的地方,都是要求很高的应用,一旦高并发场景下出现缓存失效(缓存击穿、缓存雪崩、缓存穿透),就会瞬间造成数据库访问量增大,甚至崩溃。但是现在有很多解决方案,可以帮助我们很好的避免这种情况的发生
3. 缓存与数据库数据实时同步
缓存与数据库无法做到数据的实时同步,Redis无法做到主从实时数据同步。
4. 缓存并发竞争
多个redis客户端同时对同一个key进行set值操作时,由于执行顺序引起的并发问题。(比如两个reids服务器,有一个key为y的数据,用户a和用户b同时对这个y数据修改,如果将两个请求路由到不同redis服务器,那么两个redis的值就不一样了,针对这类并发竞争问题,也需要特殊解决)
4. 缓存的读写模式
Cache Aside Pattern(旁路缓存,常用):最经典的缓存+数据库读写模式
读操作时:先读缓存,缓存没有,读数据库,然后取出数据放入缓存,同时返回响应。
更新操作:先更新数据库,然后删除缓存(缓存的值是数据结构,hash、list。更新数据需要遍历,不如直接删掉。另外这样满足懒加载特性,使用的时候才更新缓存,当然也可以异步填充缓存)。
高并发,发生脏读的三种情况(根据我们业务场景,我们总是在mysql事务提交之间,对redis操作,而不是在mysql事务提交完成之后)
1. 先更新数据库,再更新缓存。redis不能回滚,一旦数据库回滚,redis就会数据不一致,从而产生脏读。
2. 先删除缓存,再更新数据库。当删除缓存后,此时更新数据库还没有提交,就有线程来读数据,此时,缓存去数据库找数据,就会读到脏数据,除非加排它锁,及其影响效率,高并发下是不允许的。
3. 先更新数据库,再删除缓存(推荐的)。会发生脏读,更新数据库后,删除缓存,此时数据库还没提交,就有线程来读,从而刷新缓存,读到脏数据。但是可以用延迟双删策略解决,后面会讲,因此推荐这种方式。
Read/Write Through Pattern(直读/写模式),应用程序只操作缓存,缓存操作数据库,这种方式需要提供数据库的handler,开发较为复杂。
1. Read Through(穿透读/直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入缓存。
2. Write Through(穿透写/直写模式):应用程序写缓存,缓存写数据库
Write Behind Caching Pattern(后写缓存模式)
应用程序只更新缓存,缓存通过异步的方式将数据批量或合并后更新到DB中,不能实时同步,还有丢失数据的风险。
5. 缓存架构的设计思路
缓存的整体设计,需要决定的因素和问题
1. 多层次:保证分布式缓存宕机,本地缓存依然可以使用,就是各层都有缓存,并且还要备上本地缓存,分布式缓存(各个服务共用)挂了,还可以用本地缓存(每个服务自己单独用)。
2. 根据存储数据类型选择不同缓存
如果存储的是字符串或整数或二进制,值比较大(超过100K),并且只进行getter和setter,就可以使用Memcached缓存(纯内存的,多线程,K-V)。而存储复杂数据类型,比如存储的是hash、set、list、zset,就是需要存储关系、聚合、计算的数据,可以使用redis。
3. 集群方案
分布式缓存集群方案(redis)、codis、哨兵+主从、RedisCluster
4. 缓存数据的规则(需要根据需要缓存的数据,规定数据结构)
如果与数据库表一致(数据库表和缓存一一对应,缓存的字段比数据库少,只缓存经常访问的)。如果与数据库表不一致(需要存储关系,聚合,计算等,比如某个用户的贴子,用户的评论等),这些数据往往不能将数据库中的直接放在缓存中,而是规定数据结构,比如缓存评论,key可以用ID+时间戳,而value可以存储Hash类型,field为数据库中存储的id和评论内容