• 谷粒商城【成神路】-【10】——缓存


    目录

    🧂1.引入缓存的优势

    🥓2.哪些数据适合放入缓存 

    🌭3.使用redis作为缓存组件 

    🍿4.redis存在的问题 

    🧈5.添加本地锁 

    🥞6.添加分布式锁

    🥚7.整合redisson作为分布式锁


    🚗🚗🚗1.引入缓存的优势

    为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落盘工作。

    🚗🚗🚗2.哪些数据适合放入缓存 

    • 即时性、数据—致性要求不高的
    • 访问量大且更新频率不高的数据(读多,写少)

    🚗🚗🚗3.使用redis作为缓存组件 

    先确保reidis正常启动

    3.1配置redis

    • 1.引入依赖
    1. org.springframework.boot
    2. spring-boot-starter-data-redis
    • 2.配置reids信息
    1. spring
    2. redis:
    3. port: 6379
    4. host: ip地址
    5. password: XXX

    3.2优化查询 

    之前都是从数据库查询的,现在加入缓存逻辑~

    1. /**
    2. * 使用redis缓存
    3. */
    4. @Override
    5. public Map> getCatalogJson() {
    6. //1.加入缓存,缓存中存的数据全都是json
    7. String catalogJson = redisTemplate.opsForValue().get("catalogJson");
    8. if (StringUtils.isEmpty(catalogJson)) {
    9. //2.缓存中如果没有,再去数据库查找
    10. Map> catalogJsonFromDB = getCatalogJsonFromDB();
    11. //3.将数据库查到的数据,将对象转换为json存放到缓存
    12. String s = JSON.toJSONString(catalogJsonFromDB);
    13. redisTemplate.opsForValue().set("catalogJson", s);
    14. }
    15. //4.从缓存中获取,转换为我们指定的类型
    16. Map> result = JSON.parseObject(catalogJson, new TypeReference>>() {
    17. });
    18. return result;
    19. }
    20. /**
    21. * 从数据库查询并封装的分类数据
    22. *
    23. * @return
    24. */
    25. public Map> getCatalogJsonFromDB() {
    26. /**
    27. * 优化:将数据库查询的多次变为一次
    28. */
    29. List selectList = baseMapper.selectList(null);
    30. //1.查出所有1级分类
    31. List leve1Categorys = getParent_cid(selectList, 0L);
    32. //2.封装数据
    33. Map> parentCid = leve1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
    34. //1.每一个一级分类
    35. List categoryEntities = getParent_cid(selectList, v.getCatId());
    36. List catalog2Vos = null;
    37. if (categoryEntities != null) {
    38. catalog2Vos = categoryEntities.stream().map(l2 -> {
    39. Catalog2Vo vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
    40. //找二级分类的三级分类
    41. List categoryEntities3 = getParent_cid(selectList, l2.getCatId());
    42. if (categoryEntities3 != null) {
    43. List collect = categoryEntities3.stream().map(l3 -> {
    44. Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
    45. return catalog3Vo;
    46. }).collect(Collectors.toList());
    47. vo.setCatalog3List(collect);
    48. }
    49. return vo;
    50. }).collect(Collectors.toList());
    51. }
    52. return catalog2Vos;
    53. }));
    54. return parentCid;
    55. }

    3.3测试 

    在本地第一次查询后,查看redis,发现redis已经存储

    使用JMeter压测一下

    🚗🚗🚗4.redis存在的问题 

    4.1缓存穿透

    • 缓存穿透:  查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
    • 解决方案:null结果缓存,并加入短暂过期时间

    4.2缓存雪崩 

    • 缓存雪崩: 在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,大量请求全部转发到DB, DB瞬时压力过重雪崩。
    • 解决方案:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

    4.3缓存击穿 

    缓存击穿 :对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

    解决方案:枷锁    大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

    🚗🚗🚗5.添加本地锁 

    若在缓存redis中没有查到,也去数据库查询,在查询数据库时,添加本地锁,在查之前,再次判断缓存reids中是否有数据,如果有,直接返回,如果没有,在查数据库,在查完数据库后,由于延迟原因,我们查完数据库时,将数据存放到缓存中,然后在释放锁

    1. /**
    2. * 使用redis缓存
    3. */
    4. @Override
    5. public Map> getCatalogJson() {
    6. //1.使用redis缓存,存储为json对象
    7. String catalogJson = redisTemplate.opsForValue().get("catalogJson");
    8. //2.判断缓存中是否有
    9. if (StringUtils.isEmpty(catalogJson)) {
    10. System.out.println("缓存没有命中~查询数据库...");
    11. //3.如果缓存中没有,从数据库
    12. Map> catalogJsonFromDB = getCatalogJsonFromDB();
    13. return catalogJsonFromDB;
    14. }
    15. System.out.println("缓存命中....直接返回");
    16. //5.如果缓存中有,转换为我们需要的类型
    17. Map> result = JSON.parseObject(catalogJson, new TypeReference>>() {
    18. });
    19. return result;
    20. }
    21. /**
    22. * 从数据库查询并封装的分类数据
    23. *
    24. * @return
    25. */
    26. public Map> getCatalogJsonFromDB() {
    27. /**
    28. * 优化:将数据库查询的多次变为一次
    29. */
    30. //TODO 本地锁,在分布式下,必须使用分布式锁
    31. //加锁,防止缓存击穿,使用同一把锁
    32. synchronized (this) {
    33. //加所以后,我们还要去缓存中确定一次,如果缓存中没有,才继续查数据库
    34. String catalogJson = redisTemplate.opsForValue().get("catalogJson");
    35. if (!StringUtils.isEmpty(catalogJson)) {
    36. //如果缓存中有,从缓存中获取
    37. Map> result = JSON.parseObject(catalogJson, new TypeReference>>() {
    38. });
    39. return result;
    40. }
    41. System.out.println("查询了数据库~");
    42. List selectList = baseMapper.selectList(null);
    43. //1.查出所有1级分类
    44. List leve1Categorys = getParent_cid(selectList, 0L);
    45. //2.封装数据
    46. Map> parentCid = leve1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
    47. //1.每一个一级分类
    48. List categoryEntities = getParent_cid(selectList, v.getCatId());
    49. List catalog2Vos = null;
    50. if (categoryEntities != null) {
    51. catalog2Vos = categoryEntities.stream().map(l2 -> {
    52. Catalog2Vo vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
    53. //找二级分类的三级分类
    54. List categoryEntities3 = getParent_cid(selectList, l2.getCatId());
    55. if (categoryEntities3 != null) {
    56. List collect = categoryEntities3.stream().map(l3 -> {
    57. Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
    58. return catalog3Vo;
    59. }).collect(Collectors.toList());
    60. vo.setCatalog3List(collect);
    61. }
    62. return vo;
    63. }).collect(Collectors.toList());
    64. }
    65. return catalog2Vos;
    66. }));
    67. //4.将从数据库中获取的数据,转换为Json存储到redis
    68. String s = JSON.toJSONString(parentCid);
    69. //设置缓存时间,方式雪崩
    70. redisTemplate.opsForValue().set("catalogJson", s, 1, TimeUnit.DAYS);
    71. return parentCid;
    72. }
    73. }

    🚗🚗🚗6.添加分布式锁

    使用分布式锁 步骤

    • 1.先去redis中设置一个key,为了保持原子性,同时设置过期时间
    • 2.判断是否设置成功,成功则继续业务操作,失败则自选再次获取
    • 3.执行业务之后,需要删除key释放锁,为了保持原子性,使用lua脚本
    1. /**
    2. * 使用redis缓存
    3. */
    4. @Override
    5. public Map> getCatalogJson() {
    6. //1.使用redis缓存,存储为json对象
    7. String catalogJson = redisTemplate.opsForValue().get("catalogJson");
    8. //2.判断缓存中是否有
    9. if (StringUtils.isEmpty(catalogJson)) {
    10. System.out.println("缓存没有命中~查询数据库...");
    11. //3.如果缓存中没有,从数据库
    12. Map> catalogJsonFromDB = getCatalogJsonFromDBWithRedisLock();
    13. return catalogJsonFromDB;
    14. }
    15. System.out.println("缓存命中....直接返回");
    16. //5.如果缓存中有,转换为我们需要的类型
    17. Map> result = JSON.parseObject(catalogJson, new TypeReference>>() {
    18. });
    19. return result;
    20. }
    21. /**
    22. * 从数据库中获取数据,使用分布所锁
    23. *
    24. * @return
    25. */
    26. public Map> getCatalogJsonFromDBWithRedisLock() {
    27. //1.占分布式锁,去redis占位
    28. String uuid = UUID.randomUUID().toString();
    29. //2.设置过期时间,必须和加锁同步
    30. Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
    31. if (lock) {
    32. System.out.println("获取分布式锁成功..." + redisTemplate.opsForValue().get("lock"));
    33. //加锁成功...执行业务
    34. Map> dataFromDb;
    35. try {
    36. dataFromDb = getDataFromDb();
    37. } finally {
    38. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    39. //删除锁,原子性
    40. Long lock1 = redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), uuid);
    41. }
    42. return getDataFromDb();
    43. } else {
    44. //加锁失败...重试
    45. //休眠100毫秒
    46. try {
    47. Thread.sleep(2000);
    48. } catch (InterruptedException e) {
    49. }
    50. return getCatalogJsonFromDBWithRedisLock();//自旋方式
    51. }
    52. }
    53. /**
    54. * 提起方法,从数据库中获取
    55. *
    56. * @return
    57. */
    58. private Map> getDataFromDb() {
    59. //加所以后,我们还要去缓存中确定一次,如果缓存中没有,才继续查数据库
    60. String catalogJson = redisTemplate.opsForValue().get("catalogJson");
    61. if (!StringUtils.isEmpty(catalogJson)) {
    62. //如果缓存中有,从缓存中获取
    63. Map> result = JSON.parseObject(catalogJson, new TypeReference>>() {
    64. });
    65. return result;
    66. }
    67. System.out.println("查询了数据库~");
    68. List selectList = baseMapper.selectList(null);
    69. //1.查出所有1级分类
    70. List leve1Categorys = getParent_cid(selectList, 0L);
    71. //2.封装数据
    72. Map> parentCid = leve1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
    73. //1.每一个一级分类
    74. List categoryEntities = getParent_cid(selectList, v.getCatId());
    75. List catalog2Vos = null;
    76. if (categoryEntities != null) {
    77. catalog2Vos = categoryEntities.stream().map(l2 -> {
    78. Catalog2Vo vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
    79. //找二级分类的三级分类
    80. List categoryEntities3 = getParent_cid(selectList, l2.getCatId());
    81. if (categoryEntities3 != null) {
    82. List collect = categoryEntities3.stream().map(l3 -> {
    83. Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
    84. return catalog3Vo;
    85. }).collect(Collectors.toList());
    86. vo.setCatalog3List(collect);
    87. }
    88. return vo;
    89. }).collect(Collectors.toList());
    90. }
    91. return catalog2Vos;
    92. }));
    93. //4.将从数据库中获取的数据,转换为Json存储到redis
    94. String s = JSON.toJSONString(parentCid);
    95. //设置缓存时间,方式雪崩
    96. redisTemplate.opsForValue().set("catalogJson", s, 1, TimeUnit.DAYS);
    97. return parentCid;
    98. }

    🚗🚗🚗7.整合redisson作为分布式锁

    7.1引入依赖

    1. org.redisson
    2. redisson
    3. 3.12.0

    7.2程序化配置

    在配置地址时,一定要添加reds://

    1. @Configuration
    2. public class MyRedissonConfig {
    3. @Bean(destroyMethod = "shutdown")
    4. public RedissonClient redissonClient() throws IOException {
    5. //1.创建配置
    6. Config config = new Config();
    7. config.useSingleServer().setAddress("redis://192.168.20.130:6379");
    8. //2.根据config创建redisson实例
    9. RedissonClient redissonClient = Redisson.create(config);
    10. return redissonClient;
    11. }
    12. }

    7.3实例解析

    • 1.锁的自动续期,如果业务超长,运行期间自动给锁续上新的30秒,不用担心业务超长,所自动过期被删除掉
    • 2.加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30秒后自动删除
    1. @Autowired
    2. RedissonClient redissonClient;
    3. @ResponseBody
    4. @GetMapping("/hello")
    5. public String hello() {
    6. //1.设置redis的key获取一把锁,只要名字相同,就是同一把锁
    7. RLock lock = redissonClient.getLock("my-lock");
    8. //2.手动枷锁
    9. lock.lock();//阻塞式等待,默认30秒
    10. try {
    11. //3.执行业务
    12. System.out.println("枷锁成功!" + Thread.currentThread().getName());
    13. //4.模拟业务消耗时间
    14. Thread.sleep(20000);
    15. } catch (Exception e) {
    16. } finally {
    17. //3.释放锁
    18. System.out.println("释放锁~"+Thread.currentThread().getName());
    19. lock.unlock();//不删除,默认30秒后过期,自动删除
    20. }
    21. return "hello";
    22. }
    • 3.如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
    • 4.如果未指定锁的超时时间,就使用30*1000【LockWatchingTimeOut看门狗默认时间】 
      • 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔(【LockWatchingTimeOut看门狗默认时间】/3)折磨长时间自动续期;

    7.4读写锁 

    • 1.保证一定能读到最新数据,修改期间,写锁是一个排他锁,读锁是一个共享锁
    • 2.读+读:相当于并发,在redis中记录好,都会读取成功
    • 3.写+读:等待写锁释放
    • 4.写+写:阻塞方式
    • 5.读+写:有读锁,写也需要等待
    1. @GetMapping("/write")
    2. @ResponseBody
    3. public String write() {
    4. RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWrite-lock");
    5. String word = "";
    6. RLock rLock = readWriteLock.writeLock();
    7. try {
    8. //该数据加写锁,读数据加读锁
    9. rLock.lock();
    10. word = UUID.randomUUID().toString();
    11. Thread.sleep(3000);
    12. redisTemplate.opsForValue().set("writeValue", word);
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. } finally {
    16. rLock.unlock();
    17. }
    18. return word;
    19. }
    20. /**
    21. * 读写锁
    22. * @return
    23. */
    24. @GetMapping("/read")
    25. @ResponseBody
    26. public String read() {
    27. String word = "";
    28. RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWrite-lock");
    29. //加读锁
    30. RLock rLock = readWriteLock.readLock();
    31. rLock.lock();
    32. try {
    33. word = redisTemplate.opsForValue().get("writeValue");
    34. } catch (Exception e) {
    35. e.printStackTrace();
    36. }finally {
    37. rLock.unlock();
    38. }
    39. return word;
    40. }

  • 相关阅读:
    我参加第七届NVIDIA Sky Hackathon——训练ASR模型
    ADO.NET+kafka实现发布订阅保存到数据库
    成功解决:Xshell 无法连接虚拟机。如何使用Xshell连接CentOS7虚拟机(详细步骤过程)
    spdlog C++日志管理 安装和下载
    mybatis
    bugxxx
    在博客文章中使用mermaid 定义流程图,序列图,甘特图
    共话龙蜥:如何协同构建统一生态?
    修改ctags让fzf.vim插件显示C,C++方法声明的标签
    Spring Cloud zuul扩展能力设计和心得
  • 原文地址:https://blog.csdn.net/dfdg345/article/details/136508188