Redis缓存相关的几个问题

1 缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。

缓存空值,即对于不存在的数据,在缓存中放置一个空对象(注意,设置过期时间)

2 缓存击穿

缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到数据库。

加互斥锁,在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。

3 缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。

4 缓存服务器宕机

并发太高,缓存服务器连接被打满,最后挂了

  • 限流:nginx、spring cloud gateway、sentinel等都支持限流
  • 增加本地缓存(JVM内存缓存),减轻redis一部分压力

5 Redis实现分布式锁

如果用redis做分布式锁的话,有可能会存在这样一个问题:key丢失。比如,master节点写成功了还没来得及将它复制给slave就挂了,于是slave成为新的master,于是key丢失了,后果就是没锁住,多个线程持有同一把互斥锁。

必须等redis把这个key复制给所有的slave并且都持久化完成后,才能返回加锁成功。但是这样的话,对其加锁的性能就会有影响。

zookeeper同样也可以实现分布式锁。在分布式锁的的实现上,zookeeper的重点是CP,redis的重点是AP。因此,要求强一致性就用zookeeper,对性能要求比较高的话就用redis

5 示例代码

pom.xml

Product.java

package com.example.demo426.domain;

import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * @Author ChengJianSheng
 * @Date 2022/4/26
 */
@Data
public class Product implements Serializable {

    private Long productId;

    private String productName;

    private Integer stock;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

    private Integer isDeleted;

    private Integer version;
}

ProductController.java

package com.example.demo426.controller;

import com.alibaba.fastjson.JSON;
import com.example.demo426.domain.Product;
import com.example.demo426.service.ProductService;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * @Author ChengJianSheng
 * @Date 2022/4/26
 */
@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private RedissonClient redissonClient;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private ProductService productService;

    private final Cache PRODUCT_LOCAL_CACHE = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(Duration.ofMinutes(60))
            .build();

    private final String PRODUCT_CACHE_PREFIX = "cache:product:";
    private final String PRODUCT_LOCK_PREFIX = "lock:product:";
    private final String PRODUCT_RW_LOCK_PREFIX = "lock:rw:product:";

    /**
     * 更新
     * 写缓存的方式有这么几种:
     * 1. 更新完数据库后,直接删除缓存
     * 2. 更新完数据库后,主动更新缓存
     * 3. 更新完数据库后,发MQ消息,由消费者去刷新缓存
     * 4. 利用canal等工具,监听MySQL数据库binlog,然后去刷新缓存
     */
    @PostMapping("/update")
    public void update(@RequestBody Product productDTO) {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(PRODUCT_RW_LOCK_PREFIX + productDTO.getProductId());
        RLock wLock = readWriteLock.writeLock();
        wLock.lock();
        try {
            //  写数据库
            //  update product set name=xxx,...,version=version+1 where id=xx and version=xxx
            Product product = productService.update(productDTO);
            //  放入缓存
            PRODUCT_LOCAL_CACHE.put(product.getProductId(), product);
            stringRedisTemplate.opsForValue().set(PRODUCT_CACHE_PREFIX + product.getProductId(), JSON.toJSONString(product), getProductTimeout(60), TimeUnit.MINUTES);
        } finally {
            wLock.unlock();
        }
    }

    /**
     * 查询
     */
    @GetMapping("/query")
    public Product query(@RequestParam("productId") Long productId) {
        //  1. 尝试从缓存读取
        Product product = getProductFromCache(productId);
        if (null != product) {
            return product;
        }
        //  2. 准备从数据库中加载
        //  互斥锁
        RLock lock = redissonClient.getLock(PRODUCT_LOCK_PREFIX + productId);
        lock.lock();
        try {
            //  再次先查缓存
            product = getProductFromCache(productId);
            if (null != product) {
                return product;
            }

            //  为了避免缓存与数据库双写不一致
            //  读写锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(PRODUCT_RW_LOCK_PREFIX + productId);
            RLock rLock = readWriteLock.readLock();
            rLock.lock();
            try {
                //  查数据库
                product = productService.getById(productId);
                if (null == product) {
                    //  如果数据库中没有,则放置一个空对象,这样做是为了避免”缓存穿透“问题
                    product = new Product();
                } else {
                    PRODUCT_LOCAL_CACHE.put(productId, product);
                }
                //  放入缓存
                stringRedisTemplate.opsForValue().set(PRODUCT_CACHE_PREFIX + productId, JSON.toJSONString(product), getProductTimeout(60), TimeUnit.MINUTES);
            } finally {
                rLock.unlock();
            }
        } finally {
            lock.unlock();
        }

        return null;
    }

    /**
     * 查缓存
     */
    private Product getProductFromCache(Long productId) {
        //  1. 尝试从本地缓存读取
        Product product = PRODUCT_LOCAL_CACHE.getIfPresent(productId);
        if (null != product) {
            return product;
        }
        //  2. 尝试从Redis中读取
        String key = PRODUCT_CACHE_PREFIX + productId;
        String value = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(value)) {
            product = JSON.parseObject(value, Product.class);
            return product;
        }
        return null;
    }

    /**
     * 为了避免缓存集体失效,故而加了随机时间
     */
    private int getProductTimeout(int initVal) {
        Random random = new Random(10);
        return initVal + random.nextInt();
    }
}

Original: https://www.cnblogs.com/cjsblog/p/16196302.html
Author: 废物大师兄
Title: Redis缓存相关的几个问题

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/584878/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • SpringMVC 解析(五)URI链接处理

    URI在网络请求中必不可少,Spring提供了一些工具类用于解析或者生成URL,比如根据参数生成GET的URL等。本文会对Spring MVC中的URI工具进行介绍,本文主要参考S…

    Java 2023年6月8日
    083
  • 设计模式

    单例模式 饿汉式VS懒汉式 1.二者最主要的区别在于创建对象的时机不同:饿汉式是在类加载就创建了对象实例,懒汉式是在使用时才创建对象实例。 2.饿汉式不存在线程安全问题,懒汉式存在…

    Java 2023年6月5日
    0101
  • Servlet Interface

    Servlet Interface Servlet接口是Java Servlet API 核心抽象接口。大多数Servlet实现直接实现Servlet接口,或者是继承已经实现Ser…

    Java 2023年6月15日
    080
  • 浅尝Spring注解开发_自定义注册组件、属性赋值、自动装配、环境标识

    浅尝Spring注解开发,基于Spring 4.3.12包含自定义扫描组件、自定义导入组件、手动注册组件、自动注入方法和参数、使用Spring容器底层组件等 告诉Spring这是一…

    Java 2023年6月5日
    0104
  • spring boot 集成 Swagger 接口文档

    1.添加依赖 2.在 Spring Boot 配置文件中添加配置参数 3.创建配置类: Swagger 默认会根据配置的包,扫描所有接口并生成对应的 API 描述和参数信息,但这样…

    Java 2023年5月30日
    066
  • Liunx-CentOS安装MySQL8

    0 卸载 0.1 卸载原有的MariaDB 查看MariaDB安装包 rpm -qa | grep mariadb 卸载MariaDB rpm -e mariadb-libs-5….

    Java 2023年6月9日
    096
  • 微服务SpringCloud之配置中心和消息总线

    在微服务SpringCloud之Spring Cloud Config配置中心SVN博客中每个client刷新配置信息时需要post请求/actuator/refresh,但客户端…

    Java 2023年5月30日
    0102
  • MySQL中读页缓冲区buffer pool

    Buffer pool 我们都知道我们读取页面是需要将其从磁盘中读到内存中,然后等待CPU对数据进行处理。我们直到从磁盘中读取数据到内存的过程是十分慢的,所以我们读取的页面需要将其…

    Java 2023年6月16日
    082
  • SpringBoot系列之IDEA项目中设置热部署教程

    1、新建SpringBoot项目 环境准备 JDK 1.8 SpringBoot2.2.1 Maven 3.2+ 开发工具 smartGit IntelliJ IDEA2018 创…

    Java 2023年5月30日
    095
  • 软件工程师的四大台阶

    新手:前调执行力,按照方法一步步做,确保每一步不会出错就行 进阶:强调设计能力,对上级提出的要求/任务 进行抽象、 拆解(任务的拆解很有技术含量),并独立设计解决方案 高手:需要融…

    Java 2023年6月15日
    0107
  • 【0基础学java】教学日志:javaSE-面向对象6-面向对象前4章上机作业点评,主要涉及继承、封装、多态三个章节的内容

    一、面向对象第一章上机作业参考答案(略)可参考课堂Demo进行编写链接地址:https://www.cnblogs.com/yppjava/p/15690472.html二、面向对…

    Java 2023年6月6日
    087
  • 面向对象ooDay7

    .精华笔记:1)成员内部类: 应用率不高1.1)类中套类,外面的称为外部类,里面的称为内部类1.2)内部类通常只服务于外部类,对外不具备可见性1.3)内部类对象通常在外部类中创建1…

    Java 2023年6月13日
    054
  • 浏览器地址栏从输入地址到页面展示都发生了什么

    1 URL输入 2 DNS解析 浏览器会首先从缓存中找是否存在域名,如果存在就直接取出对应的ip地址, 其中会请求 第二步,本地系统缓存 如果浏览器缓存没找到,就检查本地操作系统的…

    Java 2023年6月6日
    092
  • 【Spring源码分析】Bean加载流程概览(转)

    转载自:https://www.cnblogs.com/xrq730/p/6285358.html 代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入【Spring源码分析】这个板…

    Java 2023年5月29日
    086
  • Phoenix简介及安装部署使用

    Phoenix简介及安装部署使用 posted @2017-10-14 11:33 花弄影 阅读(374 ) 评论() 编辑 Original: https://www.cnblo…

    Java 2023年6月16日
    076
  • Java你可能不知道的事(3)HashMap

    概述 HashMap对于做Java的小伙伴来说太熟悉了。估计你们每天都在使用它。它为什么叫做HashMap?它的内部是怎么实现的呢?为什么我们使用的时候很多情况都是用String作…

    Java 2023年6月13日
    061
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球