IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 《谷粒商城》开发记录 8:缓存技术 -> 正文阅读

[大数据]《谷粒商城》开发记录 8:缓存技术

一、缓存技术原理

缓存技术的核心思想是:将一些经常需要从数据库中查询的数据保存在内存中,当再次想要获得这些数据时,就可以直接从内存中返回。

1 缓存的基本功能

1.1 读模式和写模式

缓存必须要实现对读模式和写模式的支持。
● 读模式:当服务器收到一个查询请求时,会先去查缓存,如果缓存命中,就直接从缓存中返回查询结果;如果缓存未命中,再去查数据库,然后将查数据库的结果写入缓存,方便以后的查询。
● 写模式:当服务器收到一个修改请求时,在修改数据库中的数据后,要对缓存中的数据做同步修改。

缓存穿透
当客户端首次请求一个数据库中不存在的数据时,由于缓存不命中,会去查数据库,而数据库中也查不到结果。如果不将查数据库得到的null结果写入缓存,之后每次对这个不存在的数据的请求都要到数据库中做查询,缓存就失去了意义。
解决方法:将查询数据库得到的null结果写入缓存。

1.2 缓存过期

Redis官方文档:https://redis.io/commands/expire/

通常,我们在内存中创建一条缓存记录时,会指明这条记录在内存中的超时时间。否则除非用户手动删除,这条记录将永远存在于内存中,很可能造成内存泄漏。
实现缓存过期的方式一般有两种:被动方式和主动方式。两种方式需要结合使用。
● 被动方式:当客户端尝试访问一条缓存记录时,如果缓存记录已过期,就删掉这条记录并返回 null。
● 主动方式:每隔一段时间,从关联了超时时间的缓存记录中随机选取一些进行测试,删除已过期的记录。

缓存雪崩
在高并发场景下,某一时刻,大量缓存记录同时失效,对这些缓存数据的请求全部被转发到数据库,可能会导致数据库瞬时压力过大而崩溃。
解决方法:设置缓存记录的超时时间时,在原来基础上增加一个随机值。

缓存击穿
对于一些"热点"数据,在某个时刻,可能会被许多客户端同时访问,如果此时缓存记录正好失效,大量的查询请求会被全部转发到数据库,可能导致数据库崩溃。
解决方法:加锁。同一时间只允许一个线程进行查询,其他线程等待。

1.3 缓存一致性问题

参考文档:http://kaito-kidd.com/2021/09/08/how-to-keep-cache-and-consistency-of-db/

缓存一致性问题指的是:数据库中的数据更新后,缓存中的数据要和数据库中保持一致。

为什么这会是一个问题?
数据库中的数据更新、缓存数据同步更新,两个步骤不能通过一个原子操作完成。
既然是两步操作,在高并发情况下就可能出现各种问题。

针对缓存一致性问题,业界给出的解决方案是:延迟双删。
第一次删除缓存数据==>更新数据库==>等待1~5s==>第二次删除缓存数据。
● 为什么要删除缓存数据而不是直接更新?
? ?多个线程先后更新数据库后,它们不一定会按顺序更新缓存数据。
● 更新数据库前为什么要删除缓存数据?
? ?防止数据库更新成功后,某些原因导致缓存数据删除失败。
● 更新数据库后为什么要删除缓存数据?
? ?更新数据库前,可能有其他线程查询了数据库,并得到了旧数据,在更新数据库后,又将旧数据写入了缓存。
● 为什么要等待1~5s?
? ?等待查到旧数据的线程向缓存写数据完成、等待数据库主从复制完成。

除了延迟双删策略,近几年还有一种比较流行的解决方案:订阅数据库变更日志。
拿MySQL举例,当一条数据被修改时,MySQL就会生成一条变更日志(binlog),我们可以订阅这个日志,拿到具体的操作数据,然后再根据这条数据去删除对应的缓存。
目前比较成熟的订阅数据库变更日志的开源中间件:阿里的Canal。

请注意,无论是延迟双删策略还是订阅数据库变更日志策略,都只能保证缓存数据的最终一致性,无法保证强一致性。

从设计的角度来讲,我们放入缓存的数据不应该是对实时性、一致性要求很高的数据。所以我们在工程中只要做到以下两点就可以满足大部分业务场景了:
● 给缓存记录加上超时时间。
● 更新数据库后删除缓存。
而对于实时性、一致性要求高的数据,就应该查询数据库,或者加锁。

2 分布式缓存

在分布式架构中,实现缓存需要考虑的事情更多。

2.1 本地缓存的不足

写模式下,本地缓存暴露出问题:
当一台服务器对数据库中的数据进行修改后,无法对其他服务器上的本地缓存做同步更新。这样,在其他服务器处理读请求时,如果本地缓存中的数据还没有失效,就会返回修改前的旧数据。

分布式缓存的思想就是:在所有的业务服务器之外创建一块公共的缓存区域,为所有的业务服务器提供缓存服务。

2.2 分布式锁

参考文档:https://blog.csdn.net/weixin_35586546/article/details/111413184

分布式锁对分布式缓存来说是必需的吗?
不是。加锁是消耗性能的,只有对缓存一致性有要求,才需要加分布式锁。

加分布式锁可能会出现哪些问题?怎么解决?
● 执行业务逻辑异常,锁无法被释放。
? ?加锁操作放在try块中,解锁操作放在finally块中。
? ?即使解锁操作放在finally块中,也有可能因为服务器宕机而无法解锁,所以要给锁设置一个超时时间。这个超时时间一般设为业务平均执行时间的3~5倍。
? ?加锁和给锁设置超时时间两个行为必须放在一个原子操作当中,防止加锁成功后给锁设置超时时间失败。
● 执行业务逻辑期间,其他线程释放了锁。
? ?在加锁的时候,给锁附上一段验证信息,一般采用服务器ID+线程ID或者UUID。解锁时,必须校验当前线程,验证成功才能释放锁。
? ?校验和释放锁两个行为必须放在一个原子操作当中,防止校验通过后,锁正好到期,别的线程拿到锁,又被当前线程释放。
● 锁的超时时间难以估计,到了设置的超时时间业务还没有执行完。
? ?起一个守护线程,每隔一段时间就去检查一次业务逻辑有没有执行结束,如果没有,就重置锁的超时时间。这种行为被称为锁续命。
? ?如何判断业务逻辑有没有执行结束?看占有分布式锁的线程有没有加锁,如果有,说明业务逻辑还没有执行结束。
? ?注意,分布式锁本质上是一套控制流,而上文中的"加锁"是实实在在地把锁信息存在对象的markword当中。
● 分布式锁应该是可重入的。
? ?在加锁的时候需要维护当前锁的重入次数,初始值为1。解锁时,先把锁重入次数减1,当且仅当减1后的结果为0时,释放锁。
? ?在加锁的时候还要维护锁的最大超时时间,当发生锁重入时,将本次重入设置的超时时间与最大超时时间做比较,产生新的最大超时时间,并将其设为锁的真实超时时间。这是为了防止锁重入导致超时时间缩水而提前过期。

二、缓存实现方案:Spring Cache+Redis

使用Spring Cache+Redis实现缓存服务。

1 名词解释

Spring Cache:Spring缓存。
Redis:Remote Dictionary Server,远程字典服务。
RedisTemplate:Spring Boot对Redis API的封装,性能要低于直接使用Redis客户端。
Redisson:本项目选择使用的Redis客户端。
Jedis:另一个Redis客户端。
Lettuce:另一个Redis客户端。
Redis Desktop Manager:一个Redis可视化工具。

2 实现缓存的基本功能

2.1 环境搭建

1. 使用Maven引入Spring Cache。
? ? groupId: org.springframework.boot
? ? artifactId: spring-boot-starter-cache

2. 使用Maven引入Redis。
? ? groupId: org.springframework.boot
? ? artifactId: spring-boot-starter-data-redis

3. 在配置文件中配置缓存实现方式为Redis。
? ? spring.cache.type:redis
? ? spring.redis.host:192.168.56.10
? ? spring.redis.port:6379

4. 我们一般要为缓存记录指定超时时间,在配置文件中配置:
? ? spring.cache.redis.time-to-live:3600000
? ? 需要和下一步搭配起来使用。

5. 创建配置类:指定缓存数据的key的类型为String,value的类型为JSON;将查询得到的null结果写入缓存;将配置文件中的time-to-live值作为缓存记录的超时时间
? ? @Configuration // 该文件是配置文件
? ? @EnableCaching // 开启缓存功能
? ? @EnableConfigurationProperties(CacheProperties.class) // 使CacheProperties类作为配置生效
? ? public class MyCacheConfig {
? ? ? ? @Bean
? ? ? ? RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
? ? ? ? ? ? RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
? ? ? ? ? ? // 指定key的类型为String,value的类型为JSON
? ? ? ? ? ? config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
? ? ? ? ? ? config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

? ? ? ? ? ? CacheProperties.Redis redisProperties = cacheProperties.getRedis();
? ? ? ? ? ? // 将查询得到的null结果写入缓存
? ? ? ? ? ? if (!redisProperties.isCacheNullValues()) {
? ? ? ? ? ? ? ? config = config.disableCachingNullValues();
? ? ? ? ? ? }
? ? ? ? ? ? // 将配置文件中的time-to-live值作为缓存记录的超时时间
? ? ? ? ? ? if (redisProperties.getTimeToLive() != null) {
? ? ? ? ? ? ? ? config = config.entryTtl(redisProperties.getTimeToLive());
? ? ? ? ? ? }

? ? ? ? ? ? return config;
? ? ? ? }
? ? }

6. 使用注解完成开发。

2.2 @Cacheable

标注在方法上,调用方法时,如果缓存命中,直接从缓存中返回数据,如果缓存未命中,执行方法后将返回值写入缓存。

使用时必须指定缓存的名称,如@Cacheable(value="product")或者@Cacheable(cacheNames="product")或者@Cacheable("product"),三者等价。

除缓存名外,还可以指定一个key,缓存记录的完整的key是:缓存名::指定的key。
注意,必须使用SpEL表达式来指定key。例如使用方法名:key="#root.method.name"。如果要使用字符串,字符串前后要加单引号。
如果不指定key,key将使用默认值"SimpleKey []"。

2.3 @CacheEvict

标注在方法上,方法执行结束后从缓存中删除指定的记录。
使用时必须指定缓存的名称,可以指定key,指定方式和@Cacheable一致。

2.4 @CachePut

标注在方法上,方法执行结束后更新指定的缓存记录。
因为我们一般采用删除缓存的策略,所以知道是用来更新缓存的就行了。

2.5 Caching

合并多个缓存操作。

3 Redisson

官方文档:https://github.com/redisson/redisson/wiki/

3.1 环境搭建

1. 使用Maven引入redisson。
? ? groupId: org.redisson
? ? artifactId: redisson

2. 创建配置类,注入bean。
? ? @Configuration
? ? public class MyRedissonConfig{
? ? ? ? @Bean(destoryMethod="shutdown")
? ? ? ? public RedissonClient redisson() throws IOException{
? ? ? ? ? ? Config config = new Config();
? ? ? ? ? ? // 单节点模式
? ? ? ? ? ? config.useSingleServer().setAddress("redis://192.168.56.10:6379");

? ? ? ? ? ? return Redisson.create(config);
? ? ? ? }
? ? }

3. 测试。
? ? @Autowired
? ? RedissonClient redissonClient;

? ? public void test(){
? ? ? ? RLock lock = redissonClient.getLock("myLock");
? ? ? ? try{
? ? ? ? ? ? // 加锁
? ? ? ? ? ? lock.lock();
? ? ? ? ? ? Thread.sleep(30000);
? ? ? ? } catch(Exception e){
? ? ? ? } finally{
? ? ? ? ? ? // 解锁
? ? ? ? ? ? lock.unlock();
? ? ? ? }
? ? }

3.2 Redisson分布式锁

Redisson提供了各种各样的分布式锁。

● 可重入锁
? ?RLock lock = redisson.getLock("myLock");
● 公平锁
? ?RLock fairLock = redisson.getFairLock("myLock");
● 读写锁
? ?RReadWriteLock rwlock = redisson.getReadWriteLock("myRWLock");
? ?rwlock.readLock().lock();
? ?// rwlock.writeLock().lock();
● 信号量
? ?RSemaphore semaphore = redisson.getSemaphore("semaphore");
? ?semaphore.acquire();
? ?// semaphore.release();
● 闭锁
? ?RCountDownLatch latch = redisson.getCountDownLatch("myCountDownLatch");
? ?latch.trySetCount(3);
? ?latch.await();
? ?// 在其他线程里
? ?RCountDownLatch latch = redisson.getCountDownLatch("myCountDownLatch");
? ?latch.countDown();

3.3 Redisson看门狗

Redisson看门狗机制实现了锁续命。

1. 加锁时,不给锁设置超时时间,看门狗开启。

2. 看门狗会给锁设置一个超时时间,默认为30s。这个值可以在创建RedissonClient对象时,通过setLockWatchdogTimeout方法进行设置。

3. 看门狗每隔10s(lockWatchdogTimeout的三分之一)就去检查一次占有分布式锁的线程有没有加锁,如果有,就重置锁的超时时间。

3.4 Redisson执行脚本

我们希望能够组合多个Redis命令,让这些命令能够在一个原子操作内执行。Redis对此的解决方案是:支持执行Lua脚本。

Redisson执行Lua脚本示例:
? ? String luaScript = "xxx";
? ? result = redissonClient.getScript().evalSha(RScript.Mode.READ_ONLY,?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? luaScript,?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? RScript.ReturnType.VALUE,?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Collections.emptyList());

三、其他内容

1?缓存数据类型

一般使用JSON字符串。

1. 使用Maven引入fastjson。
? ? groupId: com.alibaba
? ? artifactId: fastjson

2. 在向缓存中添加缓存记录前,先将对象转换成JSON字符串。
? ? MyModel mm = new MyModel();
? ? mm.set...
? ? String data = JSON.toJSONString(mm);

3. 从缓存中查到数据后,把JSON字符串转换成目标对象。
? ? MyModel result = JSON.parseObject(data, MyModel.class);

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-05-01 15:49:32  更:2022-05-01 15:53:30 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 0:48:00-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码