cache学习

本文详细介绍了计算机存储体系的层次结构,包括寄存器、高速缓存、主存和本地磁盘的作用与特点。重点讲解了缓存(Cache)的概念、工作原理及其在减少CPU访问主存次数中的关键作用。探讨了缓存的组织结构,包括直接映射、组相联和全相联高速缓存的区别,以及缓存行(Cache Line)的管理方式。

1.计算机存储体系简介

存储器是分层次的,离CPU越近的存储器,速度越快,每字节的成本越高,同时容量也因此越小。寄存器速度最快,离CPU最近,成本最高,所以个数容量有限,其次是高速缓存(缓存也是分级,有L1,L2等缓存),再次是主存(普通内存),再次是本地磁盘。
在这里插入图片描述
寄存器的速度最快,可以在一个时钟周期内访问,其次是高速缓存,可以在几个时钟周期内访问,普通内存可以在几十个或几百个时钟周期内访问。
在这里插入图片描述
存储器分级,利用的是局部性原理。我们可以以经典的阅读书籍为例。我在读的书,捧在手里(寄存器),我最近频繁阅读的书,放在书桌上(缓存),随时取来读。当然书桌上只能放有限几本书。我更多的书在书架上(内存)。如果书架上没有的书,就去图书馆(磁盘)。我要读的书如果手里没有,那么去书桌上找,如果书桌上没有,去书架上找,如果书架上没有去图书馆去找。可以对应寄存器没有,则从缓存中取,缓存中没有,则从内存中取到缓存,如果内存中没有,则先从磁盘读入内存,再读入缓存,再读入寄存器。

2.计算机缓存 Cache

本系列的文章重点介绍缓存cache。了解如何获取cache的参数,了解缓存的组织结构。

2.1 Cache 概述

cache,中译名高速缓冲存储器,其作用是为了更好的利用局部性原理,减少CPU访问主存的次数。简单地说,CPU正在访问的指令和数据,其可能会被以后多次访问到,或者是该指令和数据附近的内存区域,也可能会被多次访问。因此,第一次访问这一块区域时,将其复制到cache中,以后访问该区域的指令或者数据时,就不用再从主存中取出。
在这里插入图片描述
cache分成多个组,每个组分成多个行,linesize是cache的基本单位,从主存向cache迁移数据都是按照linesize为单位替换的。比如linesize为32Byte,那么迁移必须一次迁移32Byte到cache。 这个linesize比较容易理解,想想我们前面书的例子,我们从书架往书桌搬书必须以书为单位,肯定不能把书撕了以页为单位。书就是linesize。当然了现实生活中每本书页数不同,但是同个cache的linesize总是相同的。

所谓8路组相连( 8-way set associative)的含义是指,每个组里面有8个行。

我们知道,cache的容量要远远小于主存,主存和cache肯定不是一一对应的,那么主存中的地址和cache的映射关系是怎样的呢?
拿到一个地址,首先是映射到一个组里面去。如何映射?取内存地址的中间几位来映射。
举例来说,data cache: 32-KB, 8-way set associative, 64-byte line size
Cache总大小为32KB,8路组相连(每组有8个line),每个line的大小linesize为64Byte,OK,我们可以很轻易的算出一共有32K/8/64=64 个组。
对于32位的内存地址,每个line有2^6 = 64Byte,所以地址的【0,5】区分line中的那个字节。一共有64个组。我们取内存地址中间6为来hash查找地址属于那个组。即内存地址的【6,11】位来确定属于64组的哪一个组。组确定了之后,【12,31】的内存地址与组中8个line挨个比对,如果【12,31】为与某个line一致,并且这个line为有效,那么缓存命中。

OK,cache分成三类,

  1. 直接映射高速缓存,这个简单,即每个组只有一个line,选中组之后不需要和组中的每个line比对,因为只有一个line。

  2. 组相联高速缓存,这个就是我们前面介绍的cache。 S个组,每个组E个line。

  3. 全相联高速缓存,这个简单,只有一个组,就是全相联。不用hash来确定组,直接挨个比对高位地址,来确定是否命中。可以想见这种方式不适合大的缓存。想想看,如果4M 的大缓存 linesize为32Byte,采用全相联的话,就意味着410241024/32 = 128K 个line挨个比较,来确定是否命中,这是多要命的事情。高速缓存立马成了低速缓存了。
    在这里插入图片描述
    描述一个cache需要以下参数:

    1. cache分级,L1 cache, L2 cache, L3 cache,级别越低,离cpu越近
    2. cache的容量
    3. cache的linesize
    4. cache 每组的行个数.

2.2 Cache 结构

假设内存容量为M,内存地址为m位:那么寻址范围为000…00~FFF…F(m位)
倘若把内存地址分为以下三个区间:
在这里插入图片描述
tag, set index, block offset三个区间有什么用呢?再来看看Cache的逻辑结构吧:
在这里插入图片描述
将此图与上图做对比,可以得出各参数如下:

B = 2^b
S = 2^s

现在来解释一下各个参数的意义:

一个cache被分为S个组,每个组有E个cacheline,而一个cacheline中,有B个存储单元,现代处理器中,这个存储单元一般是以字节(通常8个位)为单位的,也是最小的寻址单元。因此,在一个内存地址中,中间的s位决定了该单元被映射到哪一组,而最低的b位决定了该单元在cacheline中的偏移量。valid通常是一位,代表该cacheline是否是有效的(当该cacheline不存在内存映射时,当然是无效的)。tag就是内存地址的高t位,因为可能会有多个内存地址映射到同一个cacheline中,所以该位是用来校验该cacheline是否是CPU要访问的内存单元。

当tag和valid校验成功是,我们称为cache命中,这时只要将cache中的单元取出,放入CPU寄存器中即可。

当tag或valid校验失败的时候,就说明要访问的内存单元(也可能是连续的一些单元,如int占4个字节,double占8个字节)并不在cache中,这时就需要去内存中取了,这就是cache不命中的情况(cache miss)。当不命中的情况发生时,系统就会从内存中取得该单元,将其装入cache中,与此同时也放入CPU寄存器中,等待下一步处理。注意,以下这一点对理解linux cache机制非常重要:

3.计算机缓存行 CacheLine

高速缓存其实就是一组称之为缓存行(cache line)的固定大小的数据块,其大小是以突发读或者突发写周期的大小为基础的。
每个高速缓存行完全是在一个突发读操作周期中进行填充或者下载的。即使处理器只存取一个字节的存储器,高速缓存控制器也启动整个存取器访问周期并请求整个数据块。缓存行第一个字节的地址总是突发周期尺寸的倍数。缓存行的起始位置总是与突发周期的开头保持一致。

当从内存中取单元到cache中时,会一次取一个cacheline大小的内存区域到cache中,然后存进相应的cacheline中。
例如:我们要取地址 (t, s, b) 内存单元,发生了cache miss,那么系统会取 (t, s, 00…000) 到 (t, s, FF…FFF)的内存单元,将其放入相应的cacheline中。

下面看看cache的映射机制:

  • 当E=1时,每组只有一个cacheline。那么相隔2^(s+b)个单元的2个内存单元,会被映射到同一个cacheline中。(好好想想为什么?)
  • 当1<E<C/B时,每组有E个cacheline,不同的地址,只要中间s位相同,那么就会被映射到同一组中,同一组中被映射到哪个cacheline中是依赖于替换算法的。
  • 当E=C/B,此时S=1,每个内存单元都能映射到任意的cacheline。带有这样cache的处理器几乎没有,因为这种映射机制需要昂贵复杂的硬件来支持。

不管哪种映射,只要发生了cache miss,那么必定会有一个cacheline大小的内存区域,被取到cache中相应的cacheline。

现代处理器,一般将cache分为2~3级,L1, L2, L3。L1一般为CPU专有,不在多个CPU中共享。L2 cache一般是多个CPU共享的,也可能装在主板上。L1 cache还可能分为instruction cache, data cache. 这样CPU能同时取指令和数据。

下面来看看现实中cache的参数,以Intel Pentium处理器为例:

 EBSC
L1 i-cache432B12816KB
L1 d-cache432B12816KB
L2432B1024~16384128KB~2MB

 

<think>好的,我现在需要帮助用户学习Spring Cache的使用方法或教程。用户已经提供了之前的对话和几个引用,我需要根据这些信息来构建一个详细的回答。 首先,回顾用户的问题,他们明确表示想学习Spring Cache的使用方法或教程,并且希望得到指导。之前的回答中,我已经解析了Spring Cache的核心组件和常见问题解决方案,现在需要进一步提供学习路径和具体的使用教程。 接下来,我需要参考用户提供的引用内容。引用1提到了Spring Cache的基本使用,包括配置缓存管理器、注解和策略,还有示例和最佳实践。引用2讨论了缓存技术的基础和Spring Cache的关系,以及核心算法原理。引用3指出了在同一个Service类中内部调用导致缓存失效的问题。引用4解释了Redis缓存键的生成格式和如何设置前缀。引用5给出了Maven依赖的配置。 结合这些引用,我需要整理出一个结构化的教程,涵盖从环境配置到高级用法的内容。同时,用户可能遇到的常见问题也需要包含进去,比如缓存穿透、雪崩、击穿,以及内部调用失效的问题。 首先,我应该从基础开始,介绍如何添加依赖,这是项目配置的第一步,引用5已经给出了依赖配置,需要正确展示出来。然后配置缓存管理器,这里可以展示内存缓存和Redis缓存的配置方法,引用1和引用4中提到Redis的配置,需要详细说明。 接着,讲解核心注解的使用,如@Cacheable、@CachePut、@CacheEvict,每个注解都需要示例代码,确保用户能理解如何应用。同时,引用3提到的内部调用问题需要特别提醒,避免用户踩坑。 然后,高级配置部分可以包括自定义Key生成器、条件缓存、TTL设置和事务集成,这些都是实际开发中常用的技巧,引用4提到的Key前缀设置也需要在这里说明。 最后,总结最佳实践,并给出常见问题的解决方案,引用中的缓存穿透、雪崩、击穿需要结合之前的内容给出解决方法,同时强调事务一致性,如引用3中的例子。 整个过程中,需要保持回答的结构清晰,分步骤讲解,代码示例正确,并且引用相关的引用内容作为支持。同时,注意用户提到的中文回答和LaTeX格式要求,数学表达式要用$...$,独立公式用$$...$$。确保所有技术细节准确无误,并且覆盖用户可能的需求点,比如如何配置Redis的TTL,如何处理双写一致性等。 最后,生成相关问题,帮助用户进一步深入学习,问题需要基于教程内容,比如配置多级缓存、解决缓存失效、性能优化等。 检查是否有遗漏的引用内容,比如引用2中的核心算法原理,是否需要简要提及?但用户当前的问题更偏向使用教程,可能不需要深入算法,所以可以暂时略过,除非有直接关联的部分。 确保所有代码示例正确,注解使用正确,配置步骤清晰。比如在Redis配置中,使用Duration设置TTL,需要符合Spring Boot的配置方式。同时,在提到内部调用失效时,强调Spring的AOP代理机制,解释原因,并给出解决方案,如使用AopContext.currentProxy()或重新设计代码结构。 最后,确认回答符合格式要求,使用适当的标题、代码块、表格等,保持可读性。确保没有使用Markdown格式,但保留结构清晰的分段和列表。</think>### Spring Cache 使用教程(基于Spring Boot) #### 一、环境配置 1. **添加Maven依赖**(引用[5]) ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!-- Redis实现 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` 2. **启用缓存注解** ```java @SpringBootApplication @EnableCaching // 关键启用注解 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` #### 二、核心注解使用 1. **@Cacheable** - 查询缓存 ```java @Service public class BookService { // value为缓存名称,key使用SpEL表达式 @Cacheable(value = "books", key = "#isbn", unless = "#result == null") public Book getByISBN(String isbn) { // 数据库查询逻辑 } } ``` *公式推导*:缓存命中率计算 $$H = \frac{N_{cache\_hit}}{N_{total\_requests}} \times 100\%$$ 2. **@CachePut** - 更新缓存(引用[1]) ```java @CachePut(value = "books", key = "#book.isbn") public Book updateBook(Book book) { // 更新数据库后自动刷新缓存 } ``` 3. **@CacheEvict** - 删除缓存 ```java @CacheEvict(value = "books", key = "#isbn") public void deleteBook(String isbn) { // 删除数据库记录 } ``` #### 三、Redis高级配置(引用[4]) ```yaml # application.yml spring: cache: type: redis redis: time-to-live: 30m # 全局TTL key-prefix: "CACHE_" # 统一前缀 use-key-prefix: true ``` 自定义缓存配置类: ```java @Bean public RedisCacheConfiguration cacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .disableCachingNullValues() .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); } ``` #### 四、避坑指南 1. **内部调用失效问题**(引用[3]) ```java // 错误示例:直接调用导致缓存失效 public String callerMethod() { return this.cachedMethod(); // 绕过代理 } // 解决方案:通过代理对象调用 @Autowired private ApplicationContext context; public String correctCall() { MyService proxy = context.getBean(MyService.class); return proxy.cachedMethod(); } ``` 2. **缓存一致性策略** - 双写模式:先更新DB再删除缓存 - 失效模式:先删除缓存再更新DB ```java @Transactional @CacheEvict(value = "inventory", key = "#productId") public void updateStock(Long productId, int quantity) { // 先执行数据库更新 } ``` #### 五、最佳实践 1. **防御性缓存策略** ```java @Cacheable(value = "users", key = "#id", condition = "#id > 1000", // 条件过滤 unless = "#result == null") // 结果过滤 ``` 2. **多级缓存配置** ```java @Bean public CacheManager cacheManager() { return new CompositeCacheManager( new ConcurrentMapCacheManager("local_cache"), redisCacheManager() ); } ``` #### 六、调试技巧 1. 查看生成的缓存Key: 默认格式为`cacheName::key`,可通过`RedisCacheConfiguration`自定义(引用[4]) 2. 启用缓存日志: ```properties logging.level.org.springframework.cache=DEBUG ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值