Java开发之常见缓存系统

前言

  • 主要介绍在java项目开发中,如何设计缓存系统,以及需要注意的一些问题。
  • 常见框架中的缓存方式
  • Web缓存
  • 数据库缓存
  • Redis缓存

为什么需要缓存?

可以说,缓存的设计在整个IT行业无处不在,不论是硬件还是软件。从CPU寄存器、CPU L1/L2/L3级缓存,再到我们用的内存,以及硬盘里面的缓存,都是缓存系统的考虑。他们解决问题的目的只有一个:CPU速度太快,而磁盘速度太慢;CPU负责计算,磁盘负责存储。因此,CPU和磁盘不可避免的必须时时刻刻交换数据,如果每次都从磁盘获取数据,效率太低,因此引入了缓存的概念,用一个读取速度和容量处于CPU和磁盘之间,将常用数据缓存起来,加快程序处理速度。

目的:加快程序处理速度;

手段:尽量减少直接访问磁盘的次数;

常见缓存

web缓存

web缓存主要需要弄懂http头部,浏览器会根据请求头进行缓存操作。

  • Pragma
    HTTP/1.0 协议一下的旧有产物,不推荐再使用,该字段为no-cache时表示禁用缓存。

  • Expires
    GMT时间,表示缓存有效时间,时间一到就表示缓存过期了。
    如果在Cache-Control响应头设置了 “max-age” 或者 “s-max-age” 指令,那么 Expires 头会被忽略。

  • Last-Modified
    The Last-Modified 是一个响应首部,其中包含源头服务器认定的资源做出修改的日期及时间。 它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比 ETag 要低,所以这是一个备用机制。包含有 If-Modified-Since 或 If-Unmodified-Since 首部的条件请求会使用这个字段。

  • If-Modified-Since
    If-Modified-Since 是一个条件式请求首部,服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 响应,而在 Last-Modified 首部中会带有上次修改时间。 不同于 If-Unmodified-Since, If-Modified-Since 只可以用在 GET 或 HEAD 请求中。
    当与 If-None-Match 一同出现时,它(If-Modified-Since)会被忽略掉,除非服务器不支持 If-None-Match。
    最常见的应用场景是来更新没有特定 ETag 标签的缓存实体。

  • Cache-Control
    Cache-Control 通用消息头字段,被用于在http请求和响应中,通过指定指令来实现缓存机制。缓存指令是单向的,这意味着在请求中设置的指令,不一定被包含在响应中。
    缓存请求指令

    缓存响应指令

    推荐使用这个来控制缓存,如果同时存在多个缓存控制头部,则优先级从高到低分别是 Pragma -> Cache-Control -> Expires。

  • Etag
    服务器通过某个算法对资源进行计算,取得一串值用来判断是否进行了修改,之后将该值通过etag返回给客户端,客户端下次请求时通过If-None-Match或If-Match带上该值,服务器对该值进行对比校验:如果一致则不要返回资源。因此,还是需要请求服务器,由服务器来判断是否需要返回数据。
    If-None-Match和If-Match的区别是:
    If-None-Match:告诉服务器如果一致,返回状态码304,不一致则返回资源
    If-Match:告诉服务器如果不一致,返回状态码412

缓存头部总结

  • 了解每个缓存头部的特点,根据业务的特点进行合理的选择缓存头部;
  • 区分响应头和请求头;
  • 区分浏览器行为还是服务端行为;

数据库缓存

SQL解析缓存

在使用JDBC的时候,对于SQL中的参数,一定要使用变量绑定的形式进行,这样可以有效的利用SQL解析缓存,这样相同的SQL只会解析一次,大大提高SQL的解析效率。

Jdbc中使用变量绑定

String sql = “select * from tab_student where s_number=?”;
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.setString(1, “S_1001”);
ResultSet rs = pstmt.executeQuery();

使用JDBC的时候一定要尽量使用PreparedStatement进行变量绑定。

MyBatis中使用变量绑定

<select id = "selectUser" resultType="com.mybatis.User">
    select * from user where name = #{name}
</select>

这样编译出来的SQL就是这样:
select * from user where name = ?
在mybatis的xml配置中尽量不要使用${}传递参数,不仅无法使用到SQL的预编译,也存在SQL注入的风险。

MySQL查询缓存

MySQL在查询的时候会先检查查询缓存,如果缓存中存在则可以直接获取,否则从磁盘上获取。并且会自动帮我们删除失效的缓存,使用起来是非常方便快捷的。

查询缓存常用配置查看命令

  • 是否支持缓存
    SHOW VARIABLES LIKE ‘have_query_cache’;
  • 是否开启查询缓存
    SHOW VARIABLES LIKE ‘query_cache_type’;
  • 查询缓存占用大小
    SHOW VARIABLES LIKE ‘query_cache_size’;
  • 查询缓存的状态变量
    SHOW STATUS LIKE ‘Qcache%’;
    Qcache_free_blocks 查询缓存中的可用内存块数
    Qcache_free_memory 查询缓存的可用内存量
    Qcache_hits 查询缓存命中数
    Qcache_inserts 添加到查询缓存的查询数
    Qcache_lowmen_prunes 由于内存不足而从查询缓存中删除的查询数
    Qcache_not_cached 非缓存查询的数量(由于 query_cache_type 设置而无法缓存或未缓存)
    Qcache_queries_in_cache 查询缓存中注册的查询数
    Qcache_total_blocks 查询缓存中的块总数

开启方法

修改mysql配置文件/etc/my.cnf:
query_cache_type配置值:

  • off 或 0: 关闭查询缓存
  • on 或 1: 打开查询缓存,默认开启查询缓存,可以使用 SQL_NO_CACHE 关闭缓存。
  • demand 或 2: 按需打开查询缓存,需要在查询缓存的时候指定 SQL_CACHE,不指定则不缓存。
[mysqld]
query_cache_type = ON
query_cache_size = 512M

重启mysql服务即可。

指定SQL开启查询缓存方法

通过在select语句后面指定SQL_CACHE或SQL_NO_CACHE可以显示的开启和关闭查询缓存。

select SQL_CACHE count(1) from table_1;
select SQL_NO_CACHE count(1) from table_1;

注意:当query_cache_type=on或1时,默认自动开启查询缓存,不需要显示指定SQL_CACHE。

查询缓存失效的情况

  1. SQL语句必须一模一样,大小写也要相同;
  2. 如果使用不确定的值,例如:now()、user()等等时查询缓存会失效;
  3. 不查询任何表的SQL语句也无法使用查询缓存;
select 'A';
  1. 查询系统表也无法使用查询缓存;
  2. 表的更改操作会导致查询缓存失效,例如:insert/update/delete/alter table/truncate table/drop table等。

Java常见框架中的缓存

MyBatis的缓存

MyBatis也提供的缓存的支持,主要分为一级缓存和二级缓存,类似于CPU中的L1和L2级缓存。默认情况下开启了一级缓存。

MyBatis一级缓存

作用范围:同一个SQLSession内
触发缓存的条件:完全相同的查询SQL在同一个SQLSession内执行多次,那么第二次以后将会从缓存中直接获取。

刷新缓存:可以使用flushCache=ture,在每次查询后清空当前SQLSession的缓存,相当于禁用了当前SQL的一级缓存。

<select id="selectUserById" flushCache="true">
    select * 
    from user
    where id=#{id, jdbcType=INTEGER}
</select>

缓存失效机制:

  • SQLSession内的任何update、insert、delete语句都会清空缓存
  • 结合Spring使用的时候,一定要将mapper操作放入事务中,否则每个SQL就是一个独立的事务,独立的事务不共用SQLSession,因此无法使用一级缓存。
  • 如果在SQLSession中执行了commit,那么会清空该SQLSession中的所有缓存。
总结
  • MyBatis一级缓存的生命周期和SqlSession一致。
  • MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
  • MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

MyBatis二级缓存

介绍

二级缓存的级别是namespace下的,也就是可以跨SQLSession共享,默认不开启,开启二级缓存之后查询的执行流程就发生变化了,首先查询二级缓存,再查询一级缓存,最后查询数据库。

配置方式
  1. mybatis的配置文件中开启二级缓存:
<setting name="cacheEnabled" value="true"/>
  1. 在每个mapper文件中,使用cache标签进行配置,可以配置的参数如下:
  • type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
  • eviction: 定义回收的策略,常见的有FIFO,LRU。
  • flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
  • size: 最多缓存对象的个数。
  • readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
  • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
  • cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。

可以看出,相对于一级缓存来说,二级缓存的灵活性确实比一级缓存要好。

缓存原理
  • namespace级别有效
  • 事务commit之后才会写入二级缓存
  • namespace下的更新操作会导致二级缓存失效
  • 多表操作,并且不在同一个namespace下的时候,可能会出现脏读。
总结

MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

MyBatis缓存总结

分布式环境下不推荐使用MyBatis的缓存设计,推荐使用其他第三方缓存工具。

专用缓存工具-Redis

Redis介绍

简单高效的key-value存储系统,有如下数据类型:

  • String(字符串)
    二进制安全 可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M
  • Hash(字典)
    键值对集合,即编程语言中的Map类型
    适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值
  • List(列表)
    链表(双向链表) 增删快,提供了操作某一段元素的API
  • Set(集合)
    哈希表实现,元素不重复
    1、添加、删除,查找的复杂度都是O(1)
    2、为集合提供了求交集、并集、差集等操作
  • Sorted Set(有序集合)
    将Set中的元素增加一个权重参数score,元素按score有序排列,数据插入集合时,已经进行天然排序。

Redis内存淘汰机制

过期key的处理
  1. 定时删除
    可以配置redis定期检查过期的key,过期后直接删除。这里需要配置检查频率,配置redis.conf 的hz选项,默认为10(每1秒执行10次)。

  2. 惰性删除
    等客户请求的时候判断是否过期,如果过期则删除然后返回一个nil。

内存不够,如何淘汰?

当内存达到maxmemory配置时,则开始清理缓存,有如下清理机制:

  • noeviction:旧缓存永不过期,新缓存设置不了,返回错误
  • allkeys-lru:清除最少用的旧缓存,然后保存新的缓存(推荐使用)
  • allkeys-random:在所有的缓存中随机删除(不推荐)
  • volatile-lru:在那些设置了expire过期时间的缓存中,清除最少用的旧缓存,然后保存新的缓存
  • volatile-random:在那些设置了expire过期时间的缓存中,随机删除缓存
  • volatile-ttl:在那些设置了expire过期时间的缓存中,删除即将过期的

Redis单线程机制

这里的线程指的是处理用户请求的主线程是单线程的,而不是多线程的,这样的好处是不用像普通数据库一样需要加锁机制来保证一致性。

Redis持久化

RDB机制

定时将内存数据写入磁盘,RDB触发的机制有如下3种:

  • save命令:同步操作,阻塞redis服务,直到完成RDB操作为止。
  • bgsave命令:由单独子线程异步操作,操作期间主线程继续响应客户请求。
  • 自动化:通过配置来自动触发RDB bgsave操作,Redis有很多相关配置,请参考官方文档。

配置方式:
save 60 10000
在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。

AOF机制

将所有的写操作更新到日志文件中,类似数据库的归档日志,通过重放所有日志文件可以得到一份完整的内存数据。redis提供了bgrewriteaof命令,将内存中的数据用命令的方式保存到临时文件中,同时开启一个新进程来重写日志文件。重写日志文件是将内存中的数据用命令的方式写入到新的日志文件中。
触发AOF的有3种方式:

  • 每次修改就同步,同步操作,性能较差。
  • 每秒同步,异步操作,性能好,但有丢失数据的风险。
  • 从不同步

配置方式:
appendfsync always
每次有数据修改发生时都会写入AOF文件。

appendfsync everysec
每秒钟同步一次,该策略为AOF的缺省策略。

appendfsync no
从不同步。高效但是数据不会被持久化。

Redis缓存击穿

介绍

缓存中没有但是数据库中有的数据,这时由于并发用户特别多,同时都没有读取到缓存数据,导致同时去数据库查询,造成数据库压力瞬间增大。

解决方法
  1. 热点数据的缓存永不过期;
  2. 对请求进行限流、熔断、降级;
  3. 加锁,可能导致性能,需要仔细评估;
  4. 热点数据进行预热处理;

Redis缓存雪崩

介绍

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决办法
  1. 过期时间增加随机数,防止同时过期;
  2. 参考 缓存击穿的方法

Redis缓存穿透

介绍

缓存和数据库中都不存在数据,例如:数据库表的ID一般都是从1开始自增的,但是攻击者可能使用负数id进行查询,而此时缓存和数据库都不可能查询到数据,这将导致数据库压力特别大,严重可能导致数据库宕机。

解决办法
  1. 数据库返回null的数据也进行缓存,例如在redis中存放key: null形式,过期时间设置短一点,以免影响正常业务。
  2. 对业务查询条件进行判断,过滤异常查询条件,例如异常条件 id <= 0 的情况。
  3. 增加用户鉴权校验和操作频率校验。

集群与分布式

主从复制

优点:

  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离
  • 为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成
  • Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力。
  • Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求。
  • Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据

缺点:

  • Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
哨兵模式 哨兵模式

在主从复制的基础之上,引入哨兵,用来监控所有的主节点和从节点。当哨兵发现主节点宕机后,将从从节点中升级成新的主节点继续提供服务。此外,哨兵之间也可以形成集群,相互监控,从而保证整体都时高可用的。

优点:

  • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
  • 主从可以自动切换,系统更健壮,可用性更高。

缺点:

分布式

分布式集群

主从和哨兵都有一个明显的缺点,数据都是全量复制,内存不够就没法扩容了。因此,redis引入了分布式模式,将数据可以拆分给不同的节点,降低单个节点的存储压力。分布式模式采用无中心架构,所有分布式节点彼此互相连接,节点的失效是通过投票产生的,客户端可以连接任何一个节点。

数据分片原理:
Redis Cluster将整个数据库分为16384个哈希槽,缓存的每个key都属于这些槽的其中一个,再将这些槽划分给不同的分布式节点。可以使用 CLUSTER ADDSLOTS <slot> [slot...] 命令来指派节点所拥有的槽。每个缓存通过 CRC16(key) & 16383计算所属槽,然后分配给相应节点处理。

高可用:
分布式模式 + 主从节点模式来实现,主节点负责处理槽,从节点负责复制主节点的数据并在主节点下线的时候可以从从节点升级为主节点。

原理

RAFT一致性算法
gossip协议

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shadon178

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值