Redis缓存实战:Hash读写、Java对象的存取、热点数据不过期、接口限流

文章目录

  • 〇、准备工作
    • 1.搭建Redis服务
    • 2.SpringBoot整合Redis
    • 3.业务场景
  • 一、缓存对象
    • 1.功能概述
    • 2.代码实现
    • 3.功能测试
  • 二、Top N热点数据永不过期
    • 1.功能概述
    • 2.代码实现
    • 3.功能测试
  • 三、接口限流
    • 1.功能概述
    • 2.代码实现
    • 3.功能测试
  • 四、使用AOP进行限流复用
    • 1.功能概述
    • 2.代码实现
    • 3.功能测试

代码仓库(master分支):here


〇、准备工作

1.搭建Redis服务

建议使用docker,简单快捷:

docker pull redis
docker run -d --name redis -p 6379:6379 redis 
redis-cli -h localhost -p 6379

2.SpringBoot整合Redis

Java整合Redis我在之前的博客里写过了:here
在开始阅读之前,建议先自行搭建好SpringBoot工程,并基于Spring-data-redis整合Redis

  • 依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.13</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.tracy</groupId>
    <artifactId>RedisCache</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>RedisCache</name>
    <description>RedisCache</description>
    <properties>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.kie.modules/com-fasterxml-jackson -->
        <dependency>
            <groupId>org.kie.modules</groupId>
            <artifactId>com-fasterxml-jackson</artifactId>
            <version>6.5.0.Final</version>
            <type>pom</type>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

  • yml配置:
spring:
  redis:
    host: 服务器ip
    database: 0
    port: 6379

  • RedisConfig配置类:
package com.tracy.rediscache.config;
 
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
/**
 * 自定义一个redis template模板
 */
@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        //为了使开发方便,直接使用
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
 
        //Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
 
        //string的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
 
        //key采用string的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也是用string的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
 
        template.afterPropertiesSet();
 
        return template;
    }
}

3.业务场景

在这篇博客中我们将开发一个可复用的Redis缓存模块,包含的功能如下:

  • 缓存对象的读写: 通过Hash的读写实现,其中包括了Java对象与Hash的转换,并设置过期时间。
  • 应对击穿: 为了防止缓存大批过期带来的Redis击穿现象,提供Top N热点数据永不过期功能。
  • 应对穿透: 为了防止来自用户方高频率地查询缓存中并未缓存的数据导致的Redis穿透现象,设置接口限流功能来予以应对。

Redis缓存实战:Hash读写、Java对象的存取、热点数据不过期、接口限流_第1张图片

一、缓存对象

1.功能概述

  • 将任意类型的Object存入Redis Hash中: Hash key为指定的keyword与全限定类名的拼接,Hash中存储的K为属性名,V为属性值。可设置此Hash的过期时间,也可设置为永不过期。
  • 从Redis Hash中读取Object并解析成对应类型的Java Object

使用反射机制的原因是:不限制Object的类型,可将Redis缓存复用到任意项目中,反射机制为程序带来了灵活性。

2.代码实现

  • entity.Student实体类:
package com.tracy.rediscache.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Student {
    String name;
    String gender;
}

  • cache.ReadWrite读写方法实现:
package com.tracy.rediscache.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

@Component
public class ReadWrite {
    @Autowired
    RedisTemplate<String,Object> redisTemplate;

    public void write(String keyword,Object obj,int seconds) throws IllegalAccessException {
        //1 获取反射
        Class<?> c=obj.getClass();
        //2 将对象的属性和属性值存入hash,key为keyword_全限定类名的拼接
        String key=keyword+"_"+c.getName();
        for(Field field:c.getDeclaredFields()){
            String propertyName = field.getName();
            field.setAccessible(true);
            Object propertyValue = field.get(obj);
            redisTemplate.opsForHash().put(key,propertyName,propertyValue);
        }
        //3 设置过期时间,传入为0表示永不过期
        if(seconds!=0)redisTemplate.expire(key,seconds, TimeUnit.SECONDS);
    }
    public Object read(String key) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //1 获取类名
        Class<?> c=Class.forName(key.split("_")[1]);
        //2 创建一个对象
        Object obj=c.newInstance();
        //3 从redis中读取缓存,并给obj赋值
        for(Field field:c.getDeclaredFields()){
            String propertyName = field.getName();
            field.setAccessible(true);
            Object propertyValue = redisTemplate.opsForHash().get(key,propertyName);
            field.set(obj,propertyValue);
        }
        return obj;
    }
}

3.功能测试

    @Autowired
    ReadWrite readWrite;


    @Test
    void testReadWrite() throws IllegalAccessException, ClassNotFoundException, InstantiationException {
        readWrite.write("tracy",new Student("tracy","female"),1000);
        System.out.println(readWrite.read("tracy_com.tracy.rediscache.entity.Student"));
    }

Redis缓存实战:Hash读写、Java对象的存取、热点数据不过期、接口限流_第2张图片

二、Top N热点数据永不过期

1.功能概述

假设我们在此之前缓存的热点数据都设置为了永不过期,长此以往,Redis数据库的数据量会越来越大,会带来不必要的负担。
因此,在这一章节中我们基于ZSet实现一个仅保留Top N热门数据的功能。

简单来说:维护一个ZSet,每次往Redis中存入或读取一个Hash时,就在ZSet中为该Hash对应的Keyword的权重+1,顺便判断ZSet大小是否大于N,将在淘汰权重较低的ZSet元素和对应的Hash。

2.代码实现

  • 先在yml中增加变量配置:
redis:
  N: 3
  • TopN类:
package com.tracy.rediscache.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

@Component
public class TopN {
    @Autowired
    ReadWrite readWrite;
    @Autowired
    RedisTemplate<String,Object> redisTemplate;
    @Value("${redis.N}")
    int N;


    public void write(String keyword,Object obj) throws IllegalAccessException {
        //1 获取反射
        Class<?> c=obj.getClass();
        //2 将对象的属性和属性值存入hash,key为keyword_全限定类名的拼接
        String key=keyword+"_"+c.getName();
        for(Field field:c.getDeclaredFields()){
            String propertyName = field.getName();
            field.setAccessible(true);
            Object propertyValue = field.get(obj);
            redisTemplate.opsForHash().put(key,propertyName,propertyValue);
        }
        //3 将key加入ZSet
        redisTemplate.opsForZSet().addIfAbsent("TopN",key,0);
        redisTemplate.opsForZSet().incrementScore("TopN",key,1);
        //4 删除排行N之后的元素
        if(redisTemplate.opsForZSet().size("TopN")>N){
            redisTemplate.opsForZSet().popMin("TopN");
        }

    }
    public Object read(String key) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        if(Boolean.FALSE.equals(redisTemplate.hasKey(key)))return null;
        //1 获取类名
        Class<?> c=Class.forName(key.split("_")[1]);
        //2 创建一个对象
        Object obj=c.newInstance();
        //3 从redis中读取缓存,并给obj赋值
        for(Field field:c.getDeclaredFields()){
            String propertyName = field.getName();
            field.setAccessible(true);
            Object propertyValue = redisTemplate.opsForHash().get(key,propertyName);
            field.set(obj,propertyValue);
        }
        //4 将key加入ZSet
        redisTemplate.opsForZSet().addIfAbsent("TopN",key,0);
        redisTemplate.opsForZSet().incrementScore("TopN",key,1);
        //5 删除排行N之后的元素
        if(redisTemplate.opsForZSet().size("TopN")>N){
            redisTemplate.opsForZSet().popMin("TopN");
        }
        return obj;
    }
}

3.功能测试

 @Test
    void testTopN() throws IllegalAccessException, ClassNotFoundException, InstantiationException {
        topN.write("tracy1",new Student("tracy","female"));
        topN.write("tracy2",new Student("tracy","female"));
        topN.read("tracy1_com.tracy.rediscache.entity.Student");
        topN.read("tracy1_com.tracy.rediscache.entity.Student");
        topN.write("tracy3",new Student("tracy","female"));
        topN.write("tracy4",new Student("tracy","female"));
        topN.write("tracy5",new Student("tracy","female"));
        topN.write("tracy6",new Student("tracy","female"));
        //全部弹出
        while(redisTemplate.opsForZSet().size("TopN")!=0){
            System.out.println(redisTemplate.opsForZSet().popMax("TopN"));
        }

    }

Redis缓存实战:Hash读写、Java对象的存取、热点数据不过期、接口限流_第3张图片

三、接口限流

1.功能概述

针对指定接口设置QPS,简单来说,就是每次该接口被调用,在Redis中对该key的String+1(首次调用时设置一个过期时间),直到在过期时间内达到配置的最大QPS值则限制对该接口服务的调用。

在这一章节中为了方便展示功能,我们设置每分钟只能访问6次。

2.代码实现

  • yml增加配置:
redis:
  N: 3
  time: 60
  access: 6
  • 代码实现功能:
package com.tracy.rediscache.cache;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Component
public class AccessLimit {
    @Resource
    RedisTemplate<String,Integer> redisTemplate;
    @Value("${redis.time}")
    int time;
    @Value("${redis.access}")
    int access;

    public boolean accessLimit(String url){
        //1 如果是首次访问
        redisTemplate.opsForValue().setIfAbsent(url,0,time, TimeUnit.SECONDS);
        //2 +1
        redisTemplate.opsForValue().increment(url,1);
        //3 判断是否达到次数限制
        if(redisTemplate.opsForValue().get(url)>access){
            return false;
        }else{
            return true;
        }
    }
}

3.功能测试

    @Test
    void testAccessLimit(){
        for(int i=0;i<20;++i){
            System.out.println(accessLimit.accessLimit("/student"));
        }
    }

Redis缓存实战:Hash读写、Java对象的存取、热点数据不过期、接口限流_第4张图片

四、使用AOP进行限流复用

关于AOP的原理我已经在之前的博客中介绍过了:博客

1.功能概述

  • 本章节的功能概述如下:
    利用AOP编程,将接口限流的逻辑抽取出来,解耦限流逻辑和接口本身的逻辑,并达到批量拦截的作用。

2.代码实现

  • 开发一个controller用于测试本模块的功能:
package com.tracy.rediscache.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/")
public class TestController {
    @GetMapping("/search1")
    public String test1(){
        return "success";
    }
    @GetMapping("/search2")
    public String test2(){
        return "success";
    }
    @GetMapping("/search3")
    public String test3(){
        return "success";
    }
}

  • 开发一个切面实现类对controller包下的所有类中的所有方法进行拦截:

通过抛出SecurityException异常来让被拦截的方法停止执行。

execution表达式参考:博客

package com.tracy.rediscache.aop;

import com.tracy.rediscache.cache.AccessLimit;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AccessLimitAspect {
    @Autowired
    AccessLimit accessLimit;

    @Before("execution(* com.tracy.rediscache.controller..*.*(..))")
    public void checkLimit(JoinPoint joinPoint) {
        String signature=joinPoint.getTarget().getClass().getName()+"."+joinPoint.getSignature().getName()+"()";
        if(!accessLimit.accessLimit(signature)){
            throw new SecurityException("达到了限流上限!");
        }
    }
}

3.功能测试

    @Test
    void testMyAspect(){
        try{
            for(int i=0;i<10;++i){
                System.out.println(testController.test1());
            }
            for(int i=0;i<10;++i){
                System.out.println(testController.test2());
            }
            for(int i=0;i<10;++i){
                System.out.println(testController.test3());
            }
        }catch(Exception e){
            e.printStackTrace();
        }

    }

拦截成功!

Redis缓存实战:Hash读写、Java对象的存取、热点数据不过期、接口限流_第5张图片

至此,一个可复用的Redis缓存和限流模块已实现完成!

你可能感兴趣的:(存储工具,缓存,redis,哈希算法)