当前位置:   article > 正文

浅聊 缓存

浅聊 缓存

一  什么是缓存

我们在没接触过redis以前 我们是直接对数据库进行处理 也就是直接对硬盘进行读写操作,(根据我们学过的计算机硬件知识 计算机对硬盘的处理速度能力一定没有对内存的数据处理能力快)如果面对千万条数据同时查询 那么我们就会发现查询会出现卡顿的现象 ,那么缓存(redis)就应用而生 缓存是基于内存做的数据处理 速度快 所以他也继承了内存的缺点 存储小   所以我们一般都会在缓存中存放用户经常查询的数据。

二, 我们在程序中如何使用缓存

当用户发起查询操作

  • 先去redis中查询
    • 有 直接返回
    • 没有 我们就继续往下执行
  • 数据库中查询
    • 没有  那么 我们就直接返回 错误信息
    • 有     我们就要把数据存放的redis 然后返回 【记住设置好过期时间】

三 缓存更新

缓存更新机制有三种:

  • 内存淘汰机制 
    • 优点 不用自己维护 redis提供的 当内存不足时 自动淘汰大部分数据 下次查询在更新缓存
    • 缺点   一致性(redis和数据库的信息同步) 差   不可控因素太多   
    • 当我们对一致性不要求很高 就可以采用

  • 超时剔除
    • 给缓存设置TTL时间 到期后自动删除 
      • 优点   一致性 比上一个要好一点   维护成本低

  • 主动更新
    • 自己编写业务逻辑 在修改数据的时候 更新缓存
      • 优点  一致性 好 
      • 缺点   维护差

对于要求高一致性的需求 我们采用主动更新 + 超时剔除 

缓存策略:

那么 就诞生了一个问题 我们采用采用主动更新    在更新数据的时候 是删除缓存中的数据还是更新缓存中的数据?我们采用的是删除缓存  那么为什么?

读写次数    更新缓存  一直在进行相关的操作 那么我们就要一直更新缓存中的数据

                  删除缓存   只要用户已更新 我直接删除缓存 查询时再更新缓存 这样就可以避免

                                   无效的写操作(每次更新 每次就要更新缓存)

不恰当的比喻     更新缓存  你在写作业  你做的每一刻  每一步操作都在你的本子上

                 删除缓存   你把作业让别人写 在这个期间你什么都不用做  等老师要 你再把作业给老师

单体系统中 我们可以将缓存和数据库操作放在一个事务中保证缓存操作和数据库操作的一致性

在分布式系统中  利用TCC等分布式事务方案

先操作缓存还是先操作数据库呢?

答案是 先操作数据库 在删除缓存

为什么存在这样的问题呢? 高并发产生  在同一时刻 有着不同线程 进行着不同的操作 但操作的数据可能一样 

如何解决呢 因为 redis 的读写速度快 微秒级  在线程中 具有较高的执行权力 我们优先操作数据库 在删除缓存  避免线程数据碰撞 

四 缓存穿透问题

恶意进行查询(数据库和redis中都没有数据) , 服务器就会频繁访问数据库进行查询,导致一直在进行无效的查询,对数据库压力过大

解决办法

缓存空数据 如果第一次查询 redis中没有 返回数据库 数据库中没有 将空数据写入redis,

那么下一次再访问该数据 就直接在redis中查询到 直接返回了 不需要再访问数据库了

操作逻辑

请求到来 -》 redis 

                              有-》 空 / 有数据    直接返回  空看你想返回什么

                             没有-》 查询数据库

                                                 有-》直接返回数据 并写入redis

                                                没有-》 返回空 并写入空数据  设置TTL

TIP: key 设置 

 CACHE_SHOP_KEY + id;

五,缓存雪崩

在同一时间 大量key被删除 (可能设置的DDL在同一时间集体过期), redis崩了 导致大量请求到达数据库 导致数据库压力过大

  • 给不同的DDL设置随机值
  • 给redis 布置集群
  • 。。。。。。

六 缓存击穿问题

redis中存放的一个热点key突然消失 大量请求都来访问 会抵达数据库

有两种

一种是互斥锁 :给线程上一把锁锁住线程 只允许第一个请求访问 去查询数据库

其他线程等着 一直进行查询redis   尝试去回去锁  但是会失败(锁没有释放 )当第一个获取数据 就会释放锁 ,其他线程就可以从redis拿到数据了,这样避免了频繁查找数据库,但是性能差

第二种 逻辑过期 :设置了假的过期时间 只是添加了一个新的字段 不会真正删除 当一个请求来临 我们去判断数据库中的过期字段时间是否过期 如果过期了 设置一把锁 锁住线程 就开一个新的线程去查询数据库并写入redis  旧线程就直接返回旧内容, 其他访问新线程 也去查内容 访问不到 获取锁 但是会失败(锁没有释放 ),就直接返回旧数据 如果没有过期 直接返回 。一旦旧数据写入成功就返回新数据了。

程序逻辑:

请求来 --》 查询redis

                        查看redis的数据有有效期 

                                -》有效  直接返回

                                -》 无效 获取锁

                                        -》成功  

                                                --》开线程 查询数据库 写入redis   释放锁

                                                -》 返回旧数据

                                       -》失败 就直接返回旧数据

七  工具类

  1. package com.hmdp.utils;
  2. import cn.hutool.core.util.BooleanUtil;
  3. import cn.hutool.core.util.StrUtil;
  4. import cn.hutool.json.JSONObject;
  5. import cn.hutool.json.JSONUtil;
  6. import com.hmdp.dto.Result;
  7. import com.hmdp.entity.RedisData;
  8. import javafx.beans.binding.BooleanBinding;
  9. import org.springframework.data.redis.core.StringRedisTemplate;
  10. import org.springframework.stereotype.Component;
  11. import org.springframework.util.StringUtils;
  12. import javax.annotation.Resource;
  13. import java.sql.Time;
  14. import java.time.LocalDateTime;
  15. import java.util.concurrent.ExecutorService;
  16. import java.util.concurrent.Executors;
  17. import java.util.concurrent.TimeUnit;
  18. import java.util.function.Function;
  19. @Component
  20. public class CacheClient {
  21. @Resource
  22. private StringRedisTemplate stringRedisTemplate;
  23. //TODO 将java对象序列化json并存储string类型的key中 并且设置TTL过期时间
  24. public void SetByRelyTime(Object obj , String key , Long expireTime , TimeUnit unit){
  25. // 将obj转换为json对象
  26. String jsonStr = JSONUtil.toJsonStr(obj);
  27. //key + obj 存储到redis中
  28. stringRedisTemplate.opsForValue().set(key,jsonStr , expireTime ,unit);
  29. }
  30. //TODO: 将任意java对象序列化json并存储在string类型的key中 并且设置逻辑过期时间 处理缓冲击穿问题
  31. public void SetLogicTime(Object obj , String key , Long expireTime , TimeUnit unit){
  32. // 封装RedisData(有逻辑跟过期字段)
  33. RedisData redisData = new RedisData();
  34. redisData.setData(obj);
  35. redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(expireTime)));
  36. // 将java对象转换为json
  37. String jsonStr = JSONUtil.toJsonStr(redisData);
  38. //redis进行存储
  39. stringRedisTemplate.opsForValue().set(key,jsonStr);
  40. }
  41. //TODO: 根据指定key查询缓冲 并反序列化为指定类型 利用缓存空值方式解决缓存穿透问题
  42. public <R,ID> R CachePenetration(ID id , String keyPres , Class<R> type ,Function<ID,R> dbFallback,Long expireTime,TimeUnit unit ){
  43. // 拼接key
  44. String key = keyPres + id;
  45. // 根据key查询缓存
  46. String jsonData = stringRedisTemplate.opsForValue().get(key);
  47. // 判断是否为空 不为空
  48. if(StrUtil.isNotEmpty(jsonData)){
  49. R bean = JSONUtil.toBean(jsonData, type);
  50. return bean;
  51. }
  52. // 缓存穿透 如果数据库和redis中都没有 设置为空存储在redis中
  53. if (jsonData != null){
  54. // 返回错误信息
  55. return null;
  56. }
  57. //null 代表着redis中没有数据
  58. // 查询数据库
  59. R r = dbFallback.apply(id);
  60. // 解决缓存穿透问题设置 缓存和数据库中都没有数据
  61. if (r == null) {
  62. stringRedisTemplate.opsForValue().set(key,"" ,expireTime,unit);
  63. return null;
  64. }
  65. // 数据库中存在数据 把查询数据 写入redis
  66. this.SetByRelyTime(r,key,expireTime,unit);
  67. return r;
  68. }
  69. //TODO: 根据在指定的key查询缓存,并反系列化指定类型 需要利用逻辑过期解决缓存击穿问题
  70. public <R,ID> R CacheBreakdown(ID id , String KeyPrefix , Class<R> obj ,String LockPrefix,Function<ID,R> dbFallback,Long time,TimeUnit timeUnit){
  71. String key = KeyPrefix + id;
  72. // 根据id查询redis
  73. String jsonData = stringRedisTemplate.opsForValue().get(key);
  74. //redis中没有数据
  75. if(StrUtil.isEmpty(jsonData)){
  76. return null;
  77. }
  78. // redis中有数据 查看 redis的过期时间
  79. RedisData redisData = JSONUtil.toBean(jsonData, RedisData.class);
  80. JSONObject data = (JSONObject)redisData.getData();
  81. R bean = JSONUtil.toBean(data, obj); // 内容
  82. LocalDateTime expireTime = redisData.getExpireTime(); // 逻辑过期时间
  83. // 判断逻辑过期时间是否过期
  84. if(expireTime.isAfter(LocalDateTime.now())){
  85. // 没有过期 直接返回
  86. return bean;
  87. }
  88. // 过期了
  89. String lockKey = LockPrefix + id;
  90. boolean isLock = tryLock(lockKey);
  91. // 判断是否和获取锁成功
  92. if(isLock){
  93. // 成功 开启独立线程 实现缓存重建
  94. CACHE_REBUILD_EXECUTOR.submit(() -> {
  95. try {
  96. // 查询数据库
  97. R apply = dbFallback.apply(id);
  98. // 写入redis
  99. this.SetLogicTime(apply,key,time,timeUnit);
  100. } catch (Exception e) {
  101. throw new RuntimeException(e);
  102. } finally {
  103. // 释放锁
  104. unlock(lockKey);
  105. }
  106. });
  107. }
  108. // 返回过期的商铺信息
  109. return bean;
  110. }
  111. // TODO: 获取锁
  112. private boolean tryLock(String key){
  113. Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
  114. return BooleanUtil.isTrue(flag);
  115. }
  116. //TODO: 删除锁
  117. private void unlock(String key){
  118. stringRedisTemplate.delete(key);
  119. }
  120. //TODO: 线程池
  121. private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
  122. }

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号