代码仓库(master分支):here
建议使用docker,简单快捷:
docker pull redis
docker run -d --name redis -p 6379:6379 redis
redis-cli -h localhost -p 6379
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>
spring:
redis:
host: 服务器ip
database: 0
port: 6379
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;
}
}
在这篇博客中我们将开发一个可复用的Redis缓存模块,包含的功能如下:
使用反射机制的原因是:不限制Object的类型,可将Redis缓存复用到任意项目中,反射机制为程序带来了灵活性。
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;
}
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;
}
}
@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数据库的数据量会越来越大,会带来不必要的负担。
因此,在这一章节中我们基于ZSet实现一个仅保留Top N热门数据的功能。
简单来说:维护一个ZSet,每次往Redis中存入或读取一个Hash时,就在ZSet中为该Hash对应的Keyword的权重+1,顺便判断ZSet大小是否大于N,将在淘汰权重较低的ZSet元素和对应的Hash。
redis:
N: 3
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;
}
}
@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"));
}
}
针对指定接口设置QPS,简单来说,就是每次该接口被调用,在Redis中对该key的String+1(首次调用时设置一个过期时间),直到在过期时间内达到配置的最大QPS值则限制对该接口服务的调用。
在这一章节中为了方便展示功能,我们设置每分钟只能访问6次。
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;
}
}
}
@Test
void testAccessLimit(){
for(int i=0;i<20;++i){
System.out.println(accessLimit.accessLimit("/student"));
}
}
关于AOP的原理我已经在之前的博客中介绍过了:博客
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";
}
}
通过抛出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("达到了限流上限!");
}
}
}
@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缓存和限流模块已实现完成!