
缓存
一. 缓存含义
缓存主要是提高系统的性能和响应速度。当应用程序需要访问数据时,它可以首先检查缓存是否包含所需的数据,如果缓存中存在数据,则可以直接从缓存中获取,避免了对底层存储系统的访问。
二. 缓存的优点和缺点
优点:
提高系统的响应速度:通过将常用数据存储到缓存中,可以减少对数据库或其他外部资源的访问,从而提高系统的响应速度。
减轻数据库负载:通过使用缓存机制,可以减少对数据库的频繁访问,从而降低数据库的负载。
改善系统的可扩展性:使用缓存机制可以将计算结果存储在内存中,避免了频繁的计算操作,从而提高系统的可扩展性。
缺点:
额外的硬件支出。缓存是一种软件系统中以空间换时间的技术需要额外的磁盘空间和内存空间来存储数据。
缓存和主数据之间数据一致性问题。在某些场景下,同时修改数据, 如果数据库修改成功,缓存修改失败,会导致缓存和数据库数据不一致的问题.
三. 缓存的使用场景
查询结果进行缓存: 通过将从数据库查询得到的结果存储到缓存中,后续请求可以直接从缓存中读取数据,避免了频繁与数据库交互带来的性能开销。
避免重复的计算: 缓存可以用来存储计算结果,避免相同的计算过程多次执行。例如复杂的数学公式计算、日志分析,大屏展示等都可以通过缓存先前计算的结果性能。
对频繁的接口调用结果缓存: 在重复调用的接口,可以缓存调用的数据, 比如天气服务的api,地图定位的api接口数据进行缓存
存储token令牌,短信验证码, 登录失败次数等: 基于token实现登录,登录前的验证码发送,登录失败次数也都会存储在redis中。
热点数据缓存: 商品信息,用户信息, 或者ols系统的头部和底部的配置等可以提前加载到缓存中
保护数据库: 缓存还可以用作防护层,防止恶意请求直接打到数据库,造成数据库压力过大甚至崩溃。
对大量写入进行缓存: 在同一时间大量的写入或修改数据,可以先保存到缓存中,然后统一进行写导数据库.
记录用户的操作日志: 缓存可以用来暂存用户的操作记录,统一写倒数据库,而不需要实时写入数据库。
消息通知: 消息通知是频繁发生的,可以先写进缓存,然后按照一定触发条件批量写入数据库
用户的学习记录或者用户做题记录: 学习记录和做题记录的更新频繁,可以利用缓存来管理学习记录数据和做题记录,先更新缓存中的学习记录和做题记录,然后通过定时任务或异步方式批量写入数据库,以提高系统的并发能力和性能。
新闻微博的评论: 对于一些热门的文章或者博客,用户的评论是很多的,可以先写到缓存,然后异步去存到数据库中.
三. 前端缓存
localStorage,sessionStorage,cookie等等。这些功能主要用于缓存一些必要的数据,比如用户信息。比如需要携带到后端的参数。亦或者是一些列表数据等等。
localStorage和sessionStorage都是无需服务器,cookie则依赖服务器对于我们使用cookie就可以
四. 后端缓存
1. 本地缓存
本地缓存:是指和应用程序在同一个进程内的内存空间去存储数据,数据的读写都是在同一个进程内完成的。
优点:读取速度快,但是不适合进行大数据量存储。
本地缓存不需要远程网络请求去操作内存空间,没有额外的性能消耗,所以读取速度快。但是由于本地缓存占用了应用进程的内存空间,比如java进程的jvm内存空间,故不能进行大数据量存储。
缺点:
应用程序集群部署时,会存在数据更新问题(数据更新不一致)
本地缓存一般只能被同一个应用进程的程序访问,不能被其他应用程序进程访问。在单体应用集群部署时,如果数据库有数据需要更新,就要同步更新不同服务器节点上的本地缓存的数据来保证数据的一致性,但是这种操作的复杂度高,容易出错。可以基于redis的发布/订阅机制来实现各个部署节点的数据同步更新。
数据会随着应用程序的重启而丢失
因为本地缓存的数据是存储在应用进程的内存空间的,所以当应用进程重启时,本地缓存的数据会丢失。
具体的实现方式:
缓存的数据一般都是key-value键值对的数据结构,可以使用HashMap 和 ConcurretHashMap
Caffeine、Ehcache以及Guava等封装好的工具包来实现本地缓存(三者对比Caffeine性能最好)
hutool工具类有个Hutool-cache缓存工具可以使用
2. 分布式缓存(主要使用redis)
分布式缓存:分布式缓存是独立部署的服务进程,并且和应用程序没有部署在同一台服务器上,所以是需要通过远程网络请求来完成分布式缓存的读写操作,并且分布式缓存主要应用在应用程序集群部署的环境下。
优点:
支持大数据量存储
分布式缓存是独立部署的进程,拥有自身独自的内存空间,不需要占用应用程序进程的内存空间,并且还支持横向扩展的集群方式部署,所以可以进行大数据量存储。
数据不会随着应用程序重启而丢失
分布式缓存和本地缓存不同,拥有自身独立的内存空间,不会受到应用程序进程重启的影响,在应用程序重启时,分布式缓存的存储数据仍然存在。
数据集中存储,保证数据的一致性
当应用程序采用集群方式部署时,集群的每个部署节点都有一个统一的分布式缓存进行数据的读写操作,所以不会存在像本地缓存中数据更新问题,保证了不同服务器节点的数据一致性。
数据读写分离,高性能,高可用
分布式缓存一般支持数据副本机制,实现读写分离,可以解决高并发场景中的数据读写性能问题。而且在多个缓存节点冗余存储数据,提高了缓存数据的可用性,避免某个缓存节点宕机导致数据不可用问题。
缺点:
数据跨网络传输,读写性能相比本地缓存低些
分布式缓存是一个独立的服务进程,并且和应用程序进程不在同一台机器上,所以数据的读写要通过远程网络请求,这样相对于本地缓存的数据读写,性能要低一些。
分布式缓存的实现:典型实现包括 MemCached 和 Redis。因Redis相对于Memcached具有更多的功能和灵活性,支持多种数据结构、持久化机制、数据一致性和更丰富的生态系统。所以一般我们使用redis
redis是一个开源的内存数据库和缓存系统,它主要基于内存存储、单线程模型、高效的数据结构、快速的持久化策略( RDB和AOF )以及高效的网络通信(TCP协议和连接池)等特点,使其成为一个高性能的数据库和缓存系统。Redis读的速度是110000次/s,写的速度是81000次/s 。
六. 缓存在开发中的问题和开发中注意事项
缓存存在的问题
1.缓存穿透:
缓存中没有,数据库中没有: 恶意用户或攻击者通过请求不存在于缓存和后端存储中的数据来使得所有请求都落到后端存储上,导致系统瘫痪
2. 缓存雪崩:
如果Redis实例重启或发生故障,可能会导致部分或全部缓存数据丢失):缓存雪崩是指在某个时间段内,大量缓存数据同时过期或失效,导致大量请求直接访问数据库,造成数据库压力剧增,导致后端存储负载增大、响应时间变慢,甚至瘫痪。
3.缓存击穿:
缓存无,数据库有(单个key在缓存失效或过期):缓存击穿是指在高并发场景下,某个热点数据的缓存过期或不存在,导致大量请求直接访问数据库,造成数据库负载过高。从而引起系统负载暴增、性能下降甚至瘫痪。
4. 缓存保存永久数据:
数据无法及时更新,如数据库修改,则缓存与数据库数据不一致.
数据丢失的风险: 数据长时间保存在缓存中,如果缓存重启,则会导致数据丢失的问题
5. 数据一致性问题:
如果在修改数据时,修改数据库成功了,但是修改缓存失败了,就会导致数据库和缓存数据不一致的问题
6. 缓存数据量过大,缓存溢出:
如果缓存数据量过大,超过了可用的内存容量,可能会导致Redis实例崩溃或性能下降。
7. value数据特别大(大key问题):
如果一个键对应的值非常大,可能会导致Redis的性能下降,阻塞线程,并发量下降,导致客户端超时,服务端业务成功率下降.
一般value>10kb就为大key, 或list,set,hash 的元素数量> 10000为大key.
开发注意(解决方案):
1. 对空数据设置空对象缓存:
如果查询的数据为空,那也直接将空进行缓存,并设置一个比较短的过期时间. 防止缓存穿透发生(缓存和数据库都没有,导致每次请求都查询数据库).
2. 数据预热(提前加载):
将热点的数据提前存到缓存,以防高并发请求时,缓存不存在,导致数据库压力增大.
3. 设置key的过期时间不重复,加随机数(针对同时存入缓存的数据)
在存缓存时,将过期时间加上一个随机值,从而避免失效时大量的并发请求落到底层存储系统上.导致数据库崩溃
4. 使用互斥锁(mutex key)
可以设置在缓存失效时,取数据库查询时加上锁,以保证只有一个线程去访问数据库,导致数据库崩溃. 然后在其他线程获取锁失败时,进行休眠一段时间(时间取决于查询数据库时间长短), 休眠之后.在查询缓存,如果有数据,直接返回,没有数据则再次获取锁进行数据库查询.
5. 数据淘汰策略(redis方案):
当内存容量不足时,可以使用数据淘汰策略来清理一些过期或不常用的缓存数据,以腾出内存空间。常见的淘汰策略包括LRU(最近最少使用)、LFU(最不经常使用)等。可以通过配置配置文件中的 maxmemory-policy
参数来选择使用哪种淘汰策略。redis默认淘汰策略是(noeviction无淘汰)
6. 分片存储数据(redis方案):
在一些场景下,缓存的数据很庞大,可以片进行缓存, 将数据进行分割,并以不同的key进行存储. 比如: 新闻资讯的文章,, 用户的一些评论数据等.
7. 大key压缩(redis方案):
对于大key的数据进行压缩. 可以使用jdk自带的压缩工具,先序列化做Base64编码,然后在压缩,防止在不同环境中不同字符影响压缩后的结果,. 将压缩好的数据存到redis中
8. 设置缓存持久化(redis方案):
因为缓存是基于内存的,如果发生问题或者重启会导致所有数据丢失.可以设置缓存的持久化机制(RDB快照持久化, AOF记录所有的写和更新操作),防止出问题数据丢失
9. 设置集群部署(redis方案):
通过集群部署减轻单个redis的压力,提升高可用性.
七. 缓存一致性的问题和解决方案
1. 缓存更新: 先更新数据库,再删除缓存
使用在某些场景下有大批量写入的情况
优点:
保证数据库中的数据都是最新的:由于更新了数据库后再删除缓存,下次请求会重新查询数据库并更新缓存,这样可以保证缓存中存储的是最新的数据,提高了缓存命中率,加快了数据访问速度。
简化逻辑:只需要进行数据库操作然后删除缓存即可.
问题:
更新数据库成功,如果删除缓存失败或者还没有来得及删除,那么,其他线程从缓存中读取到的就是旧值,还是会发生不一致。
解决方案
使用消息队列
先更新数据库,接着将删除缓存的消息投递到mq中。自身拿到消息后,尝试进行删除缓存。如果失败,则不断进行重试。
引入消息队列,带来了可以异步重试的好处,但同时需要通过多种机制去保证删除消息不丢失。此外,该方案会对业务代码造成一定的侵入。
消息队列+订阅binlog
业务代码只操作数据库,不操作缓存。同时启动一个订阅binlog的程序去监听删除操作,然后投递到消息队列中。再启动一个消费者,根据消息去删除缓存。
在MySQL中,可以使用canal中间件来订阅binlog。
在该方案中,再次使用一个中间件来帮我们完成解耦工作,但系统的复杂度确实也在逐步上升。
使用事务(高一致性)
在更新数据库和删除缓存之前开启事务,来保证缓存和数据库一致.
为什么要删除缓存,而不是更新缓存?(防止大量写时出现问题)
数据频繁进行更新,但是一直没有去查询,那么会导致浪费资源,并且增加存储成本, 假如先更新数据库,再更新缓存。举个例子:如果数据库1小时内更新了1000次,那么缓存也要更新1000次,但是这个缓存可能在1小时内只被读取了1次,那么这1000次的更新有必要吗?反过来,如果是删除的话,就算数据库更新了1000次,那么也只是做了1次缓存删除,只有当缓存真正被读取的时候才去数据库加载。
使用删除缓存而不是修改缓存是为了防止发生并发写时写入了旧数据
2. 先更新数据库, 再更新缓存
适用于读多写少情况
问题:
在更新完数据库时更新缓存失败,导致缓存与数据库数据不一致的问题
解决方案
与先更新数据库再删除缓存类似, 可以使用相同的方案进行解决,使用事务等.
3.总结:
以上的所有方案,都是尽可能的保证数据库与缓存的一致性,也就是最终一致性. 但如果能做到强一致,那么整个系统的性能就会大打折扣。使用到缓存,就会为了提升性能。因此强一致一般与提升性能是背道而驰的。