使用IntelliJ IDEA实现SpringBoot集成Redis及高并发下缓存穿透问题处理

1. 在pom.xml文件中配置相关依赖:

        <!-- 加载MyBatis整合SpringBoot -->
        <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>

2. 在Springboot核心配置文件application.properties中配置redis连接信息:

# 配置 Redis 连接信息
spring.redis.host=localhost
spring.redis.port=6379
#spring.redis.password=

3. 在需要操作redis的类中注入redisTemplate:

package com.sztxtech.springbootmybatis.service.impl;

import com.sztxtech.springbootmybatis.entity.User;
import com.sztxtech.springbootmybatis.mapper.UserMapper;
import com.sztxtech.springbootmybatis.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
@Service
public class UserInfoServiceImpl implements UserInfoService {
    @Autowired
    private UserMapper mapper;

    /**  注入SpringBoot自动配置好的 RedisTemplate */
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    /**
     * 查询所有用户信息
     * @return
     */
    public List<User> findAllUserInfo(){
        // 查询Redis缓存
        List<User> userList = (List<User>) redisTemplate.opsForValue().get("allUser");

        //如果缓存为空,查询数据库,并把查询结果存入缓存
        if(userList == null){
            userList = mapper.selectAllUser();
            
            //将查询结果存入Redis
            redisTemplate.opsForValue().set("allUser", userList);
        }
        return userList;
    }
}

Remark:spring boot帮我们注入的redisTemplate类,泛型里面只能写 <String, String>、<Object, Object>:

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private RedisTemplate<Object, Object> redisTemplate;

以上就是通过IntelliJ IDEA实现SpringBoot集成Redis缓存的过程。在Redis中缓存的具体结果如下:

4. 由于Redis在存储数据时,Key和Value默认都是进行过序列化的,数据可读性不高。为解决此问题,可以在做Redis缓存之前对数据做相应的处理,即通过 org.springframework.data.redis.serializer.RedisSerializer 和  org.springframework.data.redis.serializer.StringRedisSerializer 创建序列化器,通过 RedisTemplate 的 setKeySerializer 方法,传入创建的序列化器,对存储缓存的序列化方式进行修改;具体实现如下:

  public List<User> findAllUserInfo(){
        // 字符串的序列化器
        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);

        // 查询Redis缓存
        List<User> userList = (List<User>) redisTemplate.opsForValue().get("allUser");
        if(userList == null){
            userList = mapper.selectAllUser();
            redisTemplate.opsForValue().set("allUser", userList);
        }
        return userList;
    }

清除Redis中之前存入的数据,再次进行缓存的结果如下:

可以看到,处理后的缓存结果,Key就是在Java代码中设置的“allUser”。

5. 针对以上的查询功能,假设同时有1W人同时执行此首次查询(高并发)功能,那么,Redis缓存中是没有数据的,这样这1W人的查询就都会查询数据库.....所以数据库的压力会剧增;同时,这种情况下,Redis缓存并没起到作用,这就出现了缓存穿透问题。

        解决此问题可以使用多线程的线程同步来进行处理。

        方式一:直接在查询方法上加 synchronized 进行线程同步处理;这样,当1W人执行查询时,一次只有1人能进入该查询方法,其余的查询请求在此方法处于等待状态,从第二个查询进入此方法开始,开始查询Redis缓存。但是这种处理方式,系统的效率会比较低,牺牲性能来降低数据库查询量!

    public synchronized List<User> findAllUserInfo(){
        // 字符串的序列化器
        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);

        // 查询Redis缓存
        List<User> userList = (List<User>) redisTemplate.opsForValue().get("allUser");
        if(userList == null){
            userList = mapper.selectAllUser();
            redisTemplate.opsForValue().set("allUser", userList);
        }
        return userList;
    }

        方式二:由于Spring容器中的Bean默认都是单例的,可以使用线程锁来实现:

    public List<User> findAllUserInfo(){
        // 字符串的序列化器
        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);

        // 查询Redis缓存
        List<User> userList = (List<User>) redisTemplate.opsForValue().get("allUser");
        // 双重检测锁
        if(userList == null){
            synchronized (this){
                userList = (List<User>) redisTemplate.opsForValue().get("allUser");
                if(userList == null){
                    userList = mapper.selectAllUser();
                    redisTemplate.opsForValue().set("allUser", userList);
                }
            }
        }
        return userList;
    }

对方式二的实现进行测试:

在Controller中创建测试方法,通过线程池模拟多线程的方式进行测试:

    @GetMapping("/boot/findAllUser")
    public void findAllUserInfomation(){

        //创建线程,调用Service层的查询方法
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                service.findAllUserInfo();
            }
        };

        //创建线程池; 通过多线程测试缓存穿透问题
        ExecutorService executorService = Executors.newFixedThreadPool(25);
        for(int i=0; i<=10000; i++){
            executorService.submit(runnable);
        }
    }

为了体现测试效果,得知程序是查询数据还是Redis缓存,对以上查询功能的Service实现类稍做修改(添加输出语句打印查询过程):

    public List<User> findAllUserInfo(){
        // 字符串的序列化器
        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);

        // 查询Redis缓存
        List<User> userList = (List<User>) redisTemplate.opsForValue().get("allUser");
        // 双重检测锁
        if(userList == null){
            synchronized (this){
                userList = (List<User>) redisTemplate.opsForValue().get("allUser");
                if(userList == null){
                    userList = mapper.selectAllUser();
                    redisTemplate.opsForValue().set("allUser", userList);
                    System.out.println("synchronized代码块内 查询数据库!!!!!!");
                }else {
                    System.out.println("synchronized代码块内 查询Redis缓存 ......");
                }
            }
        }else {
            System.out.println("查询Redis缓存 ......");
        }
        return userList;
    }

浏览器访问该查询路径,控制台输出的查询过程如下【只有第一次查询的是数据库,之后全部查询Redis缓存】:

 

而未使用线程同步处理缓存穿透之前的查询过程如下,可以看到有大量查询数据的记录,这并不是使用Redis缓存想要的效果:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值