详解Spring Cache使用Redisson分布式锁解决缓存击穿问题

目录
  • 1 什么是缓存击穿
  • 2 为什么要使用分布式锁
  • 3 什么是Redisson
  • 4 Spring Boot集成Redisson
    • 4.1 添加maven依赖
    • 4.2 配置yml
    • 4.3 配置RedissonConfig
  • 5 使用Redisson的分布式锁解决缓存击穿

1 什么是缓存击穿

一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。

2 为什么要使用分布式锁

在项目中,当共享资源出现竞争情况的时候,为了防止出现并发问题,我们一般会采用锁机制来控制。在单机环境下,可以使用synchronized或Lock来实现;但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现,比如mysql、redis、zookeeper。

3 什么是Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

4 Spring Boot集成Redisson

4.1 添加maven依赖

不再需要spring-boot-starter-data-redis依赖,但是都添加也不会报错

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>

4.2 配置yml

spring:
  datasource:
    username: xx
    password: xxxxxx
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=CTT
  cache:
    type: redis
  redis:
    database: 0
    port: 6379               # Redis服务器连接端口
    host: localhost          # Redis服务器地址
    password: xxxxxx         # Redis服务器连接密码(默认为空)
    timeout: 5000            # 超时时间

4.3 配置RedissonConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.Random;

@EnableCaching
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean(destroyMethod = "shutdown")  // bean销毁时关闭Redisson实例,但不关闭Redis服务
    public RedissonClient redisson() {
        //创建配置
        Config config = new Config();
        /**
         *  连接哨兵:config.useSentinelServers().setMasterName("myMaster").addSentinelAddress()
         *  连接集群: config.useClusterServers().addNodeAddress()
         */
        config.useSingleServer()
                .setAddress("redis://" + host + ":" + port)
                .setPassword(password)
                .setTimeout(5000);
        //根据config创建出RedissonClient实例
        return Redisson.create(config);
    }

    @Bean
    public CacheManager RedisCacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        /**
         * 新版本中om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL)已经被废弃
         * 建议替换为om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL)
         */
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化解决乱码的问题
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 设置缓存过期时间  为解决缓存雪崩,所以将过期时间加随机值
                .entryTtl(Duration.ofSeconds(60 * 60 + new Random().nextInt(60 * 10)))
                // 设置key的序列化方式
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                // 设置value的序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        // .disableCachingNullValues(); //为防止缓存击穿,所以允许缓存null值
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                // 启用RedisCache以将缓存 put/evict 操作与正在进行的 Spring 管理的事务同步
                .transactionAware()
                .build();
        return cacheManager;
    }
}

5 使用Redisson的分布式锁解决缓存击穿

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.company.dubbodemo.entity.User;
import com.company.dubbodemo.mapper.UserMapper;
import com.company.dubbodemo.service.UserService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {
    
    @Resource
    private RedissonClient redissonClient;

    @Resource
    private UserMapper userMapper;

    @Override
    // 一定要设置sync = true开启异步,否则会导致多个线程同时获取到锁
    @Cacheable(cacheNames = "user", key = "#id", sync = true)
    public User findById(Long id) {
        /**
         *
         * 加了@Cacheable之后方法体执行说明缓存中不存在所查询的数据
         * 获取一把锁,只要锁的名字一样,就是同一把锁
         */

        /**
         * 注意: 如果设置了lock.lock(10,TimeUnit.SECONDS) 锁过期不会自动续期
         *      1、如果我们传递了锁的过期时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
         *      2、如果没有指定锁的超时时间,就使用30000L(LockWatchdogTimeout 看门狗的默认时间)
         *      可通过RedissonConfig-->getRedissonClient()-->config.setLockWatchdogTimeout()设置看门狗时间
         *         只要占锁成功就会启动一个定时任务【就会重新给锁设置过期时间,新的时间就是看门狗的默认时间】,每隔10s都会自动续期,续期成30s
         * 看门狗机制
         * 1、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心因为业务时间长,锁自动过期被删除
         * 2、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除
         *
         */
        RLock lock = redissonClient.getLock("redissonClient-lock");

        // 对第一个线程执行方法体的线程加锁,加了@Cacheable,方法执行之后会将方法的返回值存入缓存,下一个线程直接读取缓存
        lock.lock();
        User user = new User();
        try {
            user = userMapper.selectById(id);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return user;
    }
}

到此这篇关于详解Spring Cache使用Redisson分布式锁解决缓存击穿问题的文章就介绍到这了,更多相关Spring Cache 缓存击穿内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 使用spring-cache一行代码解决缓存击穿问题

    目录 引言 正文 目前缺陷 真正方案 缓存穿透 缓存击穿 缓存雪崩 文末 引言 今天,重新回顾一下缓存击穿这个问题! 之所以写这个文章呢,因为目前网上流传的文章落地性太差(什么布隆过滤器啊,布谷过滤器啊,嗯,你们懂的),其实这类方案并不适合在项目中直接落地. 那么,我们在项目中落地代码的时候,其实只需要一个注解就能解决这些问题,并不需要搞的那么复杂. 本文有一个前提,读者必须是java栈,且是用Springboot构建自己的项目,如果是go技术栈或者python技术栈的,可能介绍的思路仅供大家参

  • 详解Spring Cache使用Redisson分布式锁解决缓存击穿问题

    目录 1 什么是缓存击穿 2 为什么要使用分布式锁 3 什么是Redisson 4 Spring Boot集成Redisson 4.1 添加maven依赖 4.2 配置yml 4.3 配置RedissonConfig 5 使用Redisson的分布式锁解决缓存击穿 1 什么是缓存击穿 一份热点数据,它的访问量非常大.在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃. 2 为什么要使用分布式锁 在项目中,当共享资源出现竞争情况的时候,为了防止出现并发问题,我们一般会采用锁机制来控制.在单机环境

  • Redis分布式锁防止缓存击穿的实现

    缓存击穿 和缓存穿透不同的是,缓存击穿是指:缓存中没有,但是数据库中存在的热点数据. 例如:首页的热点新闻,并发访问量非常大的热点数据,如果缓存过期失效,服务器会去查询DB,这时候如果大量的并发去查询DB,可能会瞬间压垮DB. 画了个简图,如下所示: 解决方案:DB查询加分布式锁. 未加锁的情况 解决问题之前,先看一下不做处理的代码和运行情况. 根据商品ID查询商品详情代码 清空Redis缓存,开启5个线程去并发访问测试,测试代码如下: 我们预期希望DB只查询一次,后面4个查询从Redis缓存中

  • 一文详解如何使用Redis实现分布式锁

    目录 1. 什么是分布式锁 2. 使用Redis实现分布式锁 2.1 加锁 2.2 释放锁 2.3 给锁设置有效期 2.4 给锁设置唯一值 2.5 通过LUA脚本实现释放锁的原子性 3. 小结 1. 什么是分布式锁 当我们在编写多线程代码的时候,不同的线程可能会发生资源的争夺,为了避免资源争夺造成的错误,我们会对资源上锁,只有获得锁的线程才能继续往下执行. 进程中的锁,本质就是内存中一个变量,当一个线程执行某个操作申请加锁时,如果能成功把代表锁的变量值设置为1,则表示获得了锁,其他线程想要获得锁

  • 详解Spring关于@Resource注入为null解决办法

    初学spring,我在dao层初始化c3p0的时候,使用@Resource注解新建对象是发现注入为null,告诉我 java.lang.NullPointerException. @Repository public class UserDaoImpl implements UserDao { @Resource ComboPooledDataSource dataSource; 反复检查了配置文件,没有发现任何问题,最后在同学的帮助下终于发现了问题的所在,特记录于此,一个教训. 问题就出在我的

  • 详解Spring中实现接口动态的解决方法

    前言 本文主要给大家介绍的是关于Spring实现接口动态的相关内容,分享出来供大家参考学习,下面话不多说,来一起看看详细的介绍吧. 关于这个问题是因为领导最近跟我提了一个需求,是有关于实现类Mybatis的@Select.@Insert注解的功能.其是基于interface层面,不存在任何的接口实现类.因而在实现的过程中,首先要解决的是如何动态实现接口的实例化.其次是如何将使接口根据注解实现相应的功能. 声明 解决方案是基于Mybatis源码,进行二次开发实现. 解决方法 我们先来看看Mybat

  • 详解Spring Boot 2.0.2+Ajax解决跨域请求的问题

    问题描述 后端域名为A.abc.com,前端域名为B.abc.com.浏览器在访问时,会出现跨域访问.浏览器对于javascript的同源策略的限制. HTTP请求时,请求本身会返回200,但是返回结果不会走success,并且会在浏览器console中提示: 已拦截跨源请求:同源策略禁止读取位于 https://www.baidu.com/ 的远程资源.(原因:CORS 头缺少 'Access-Control-Allow-Origin'). 解决方案 1.jsonp 2.引用A站的js 3.N

  • Redisson分布式锁之加解锁详解

    目录 引言 锁的可重入性 加锁 锁续命 释放锁 引言 2023的金三银四来的没想象中那么激烈,一个朋友前段时间投了几十家,多数石沉大海,好不容易等来面试机会,就恰好被问道项目中关于分布式锁的应用,后涉及Redisson实现分布式锁的原理,答不上来. 锁的可重入性 我们都知道,Java中synchronized和lock都支持可重入,synchronized的锁关联一个线程持有者和一个计数器.当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1.此时其他线程请求该锁,则必须等待.而该持

  • 详解spring cloud config整合gitlab搭建分布式的配置中心

    在前面的博客中,我们都是将配置文件放在各自的服务中,但是这样做有一个缺点,一旦配置修改了,那么我们就必须停机,然后修改配置文件后再进行上线,服务少的话,这样做还无可厚非,但是如果是成百上千的服务了,这个时候,就需要用到分布式的配置管理了.而spring cloud config正是用来解决这个问题而生的.下面就结合gitlab来实现分布式配置中心的搭建.spring cloud config配置中心由server端和client端组成, 前提:在gitlab中的工程下新建一个配置文件config

  • 详解spring与shiro集成

    Shiro的组件都是JavaBean/POJO式的组件,所以非常容易使用Spring进行组件管理,可以非常方便的从ini配置迁移到Spring进行管理,且支持JavaSE应用及Web应用的集成. 在示例之前,需要导入shiro-spring及spring-context依赖,具体请参考pom.xml. spring-beans.xml配置文件提供了基础组件如DataSource.DAO.Service组件的配置. JavaSE应用  spring-shiro.xml提供了普通JavaSE独立应用

  • 详解spring cloud config实现datasource的热部署

    关于spring cloud config的基本使用,前面的博客中已经说过了,如果不了解的话,请先看以前的博客 spring cloud config整合gitlab搭建分布式的配置中心 spring cloud config分布式配置中心的高可用 今天,我们的重点是如何实现数据源的热部署. 1.在客户端配置数据源 @RefreshScope @Configuration// 配置数据源 public class DataSourceConfigure { @Bean @RefreshScope

随机推荐