Springboot(三)、缓存中间件 Redis 的使用

本贴最后更新于 1594 天前,其中的信息可能已经时移世易

Springboot(三)、缓存中间件 Redis 的使用

技术选用

  • springboot
  • guava
  • redis
  • postman
  • AnotherRedisManager

整合 Redis

  • 首先启动 redis 服务,关于 redis 的安装和启动这里不再赘述。image.png
  • 引入 redis 依赖以及 guava 工具包,

在父模块定义版本

<course.guava.version>18.0</course.guava.version>

在 server 模块引入依赖

<!-- 引入redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${course.spring-boot.sersion}</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${course.guava.version}</version> </dependency>
  • 在 yaml 文件中添加 redis 配置
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.119.110:3306/course?characterEncoding=UTF-8 username: root password: 123456 redis: port: 6379 host: 192.168.119.110 # 这里填写自己的redis的ip和端口、密码 password: 123456 mybatis: mapper-locations: classpath:sqlmap/**/*.xml type-aliases-package: com.to.jing.course.sdk
  • 创建 javaConfigure 配置
    新建包 com.to.jing.course.server.config,在包下创建 RedisConfig.java 文件
package com.to.jing.course.server.config; import org.springframework.beans.factory.annotation.Autowired; 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.core.StringRedisTemplate; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public RedisTemplate<String,Object> redisTemplate(){ RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); //指定key序列化策略为String序列化,Value为JDK自带的序列化策略 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); //指定hashKey序列化策略为String序列化 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); return redisTemplate; } @Bean public StringRedisTemplate stringRedisTemplate(){ // 采用默认配置即可,后续有自定义注入配置时在此处添加即可 StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); stringRedisTemplate.setConnectionFactory(redisConnectionFactory); return stringRedisTemplate; } }

这里我们主要引入两个对象 RedisTemplate 和 StringRedisTemplate,指定缓存 key 与 value 的序列化策略。StringRedisTemplate 是对 RedisTemplate 的默认使用 String 序列化的实现,我们可以从 StringRedisTemplate 看到源码:

image.png

  • 单元测试
    在 server 模块下 test/java 目录下创建 RedisTest.java 文件
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.to.jing.course.sdk.User; import com.to.jing.course.server.AppServer; import com.to.jing.course.service.UserService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.ListOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.ArrayList; import java.util.List; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = AppServer.class) public class RedisTest { @Autowired private RedisTemplate<String,Object> redisTemplate; @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private UserService userService; @Test public void one(){ final String key = "redis_test"; final String content = "这是一个redis测试"; redisTemplate.opsForValue().set(key,content); Object result = redisTemplate.opsForValue().get(key); System.out.println(result); } /** * 测试对象 */ @Test public void user(){ User user = userService.findUserById(1); final String key = "test_user"; stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(user)); User user1 = JSONObject.parseObject(stringRedisTemplate.opsForValue().get(key), User.class); System.out.println(user1); } /** * 测试集合 */ @Test public void testList(){ List<User> list = new ArrayList<>(); list.add(userService.findUserById(1)); final String key = "test_list"; stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(list)); System.out.println(JSONObject.parseArray(stringRedisTemplate.opsForValue().get(key),User.class)); } /** * 测试list */ @Test public void testRedisList(){ final String key = "test_redis_list"; ListOperations<String, Object> stringObjectListOperations = redisTemplate.opsForList(); for (int i = 0; i < 2 ; i++){ User user = new User(i + 1,"hah","aa",23,true); stringObjectListOperations.leftPush(key,user); } Object res = stringObjectListOperations.rightPop(key); User temp; while (res != null){ temp = (User) res; System.out.println(temp); res = stringObjectListOperations.rightPop(key); } } /** * list pushALL */ @Test public void testRedisPushAll(){ final String key = "test_push_all"; List<User> users = new ArrayList<>(); for (int i = 0 ;i< 2;i++){ users.add(new User(i + 1,"hah","aa",23,true)); } redisTemplate.opsForList().leftPushAll(key,users); List<Object> range = redisTemplate.opsForList().range(key, 0, -1); System.out.println(range); } }

注意,这里会显示一些包引入错误,原因是之前引入 springboot-test 模块时配置错了,这里将原来的 pom 文件的依赖替换一下。原来的为:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-test</artifactId> <version>${course.spring-boot.sersion}</version> </dependency>

替换后为:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${course.spring-boot.sersion}</version> <scope>test</scope> </dependency>

替换后还是会显示错误,我们去 User 类下添加一些构造器,修改后代码如下,主要使用了 lombok 中的无参构造 @NoArgsConstructor 和全参构造 @AllArgsConstructor

package com.to.jing.course.sdk.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private Integer id; private String username; private String password; private Integer age; private Boolean disable; public static User Null(){ User user = new User(); user.setDisable(false); return user; } }

然后我们再运行上面的单元测试可以看到测试通过
image.png
关于使用 redisTemplate 对 redis 中 string,list,set,zset,hash 数据结构的使用这里也不再详细说明

缓存穿透实战

在 redis 使用中常见的问题有缓存穿透、缓存雪崩、缓存击穿、数据一致性的问题。

  • 缓存雪崩:指的是在某个时间点,缓存中的 key 集体过期失效,致使大量查询数据的请求都落在了数据库上,导致数据库负载过高,压力暴增,甚至有可能压垮数据库。这种问题产生的原因其实主要是大量的 key 再扣个时间点或者时间段过期失效,所以为了更好的避免这种情况的发生,一般的做法是为这些 key 设置不同的,随机的 TTL,从而错开缓存 key 的失效时间点,可以在某种程度上减轻数据库的查询压力。
  • 缓存击穿:指的是缓存中某个频繁被访问的 key(可以被称为“热点 key”)在不停地扛着前端地高并发请求,当这个 key 突然在某个瞬间过期失效,持续地高并发访问请求就穿破缓存,直接访问数据库,导致数据库压力在某一瞬间暴增。一般解决方案是不设置过期时间,但有些数据需要设置怎么办,可以使用分布式锁加策略的方式。因为分布式锁只允许当前一个请求去查库,锁比较重,这种时候可以让其它请求直接返回,或者拿着上一个时间段的热点数据进行返回。
  • 缓存穿透:指的是所查询的数据不存在缓存中也不存在数据库中,这样每次请求过来都会经过缓存再落在数据库上,缓存的效果就没了。如果前端频繁发起访问请求,恶意请求数据库中不存在的 key,则此时数据库中查询到的数据将永远为 null,若被恶意攻击,发起“洪流”式查询,则很有可能会对数据库造成极大的压力,甚至压垮“数据库”。一般的解决方法是将 null 结果也缓存在 redis 中,并设置过期时间,或者用 nginx 对恶意 ip 进行黑白名单设置等等。
  • 数据一致性:顾名思义就是缓存和数据库的数据保持一致。这种一般是对于需要更新数据来说,我们应该是先更新数据库,然后再对 redis 键进行删除操作。这样前端进行查询的时候,自然会把新的数据读进缓存里。

这里,我们通过代码实战一下缓存穿透。

首先,我们创建一个 RedisService 封装一下 RedisTemplate 和 StringRedisTemplate,在包 com.to.jing.course.server.service 下。

package com.to.jing.course.server.service; import com.alibaba.fastjson.JSON; import com.google.common.base.Strings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.concurrent.TimeUnit; @Service public class RedisService { @Autowired private RedisTemplate<String,Object> redisTemplate; @Autowired private StringRedisTemplate stringRedisTemplate; public void setObject(String key,Object o){ redisTemplate.opsForValue().set(key,o); } public void setString(String key,Object o){ stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(o)); } public void setString(String key, Object o, Long time, TimeUnit timeUnit){ stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(o),time,timeUnit); } public Object getObject(String key){ return redisTemplate.opsForValue().get(key); } public <T> T getString(String key , Class<T> clazz){ String value = stringRedisTemplate.opsForValue().get(key); if (value == null || Strings.isNullOrEmpty(value)){ return null; } return JSON.parseObject(value,clazz); } public Boolean hasKey(String key){ return redisTemplate.hasKey(key); } }

创建 redis 键值前缀接口,主要定义一些唯一键值。在 server 模块新建包 com.to.jing.course.server.common,新建 RedisPrefix 接口。

package com.to.jing.course.server.common; /** * redis键前缀 */ public interface RedisPrefix { /** * 用户缓存键 */ String COURSE_CACHE_USER = "course_cache_user:"; }

然后在 service 模块 userService 接口中添加方法 getUserInfo。

package com.to.jing.course.service; import com.to.jing.course.sdk.domain.User; public interface UserService { User findUserById(Integer id); User getUserInfo(Integer id); }

server 模块 userServiceImpl 对其实现,使用 lombok 的 @Slf4j 注解添加日志。

package com.to.jing.course.server.service.impl; import com.to.jing.course.dao.UserDao; import com.to.jing.course.sdk.domain.User; import com.to.jing.course.server.common.RedisPrefix; import com.to.jing.course.server.service.RedisService; import com.to.jing.course.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Objects; import java.util.concurrent.TimeUnit; @Service @Slf4j public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Autowired private RedisService redisService; @Override public User findUserById(Integer id) { return userDao.findUserById(id); } @Override public User getUserInfo(Integer id) { final String key = RedisPrefix.COURSE_CACHE_USER + id; User user = null; if (redisService.hasKey(key)){ log.info("从缓存中获取用户信息"); user = redisService.getString(key,User.class); }else { log.info("从数据库中获取用户信息"); user = userDao.findUserById(id); if (Objects.isNull(user)){ //用户不存在,缓存其空对象 user = User.Null(); user.setUsername("无效用户"); redisService.setString(key,user,30L, TimeUnit.MINUTES); }else{ redisService.setString(key,user); } } return user; } }

其方法中的主要逻辑是先查看 redis 中是否存在键值,存在就获取 redis 中的数据返回,不存在就查库并设置到缓存 redis 中,缓存穿透的解决就是遇到从数据库查询不到的数据也将空缓存到 redis 中并设置一定的过期时间,访问频繁地访问数据库。
在 sdk 模块创建 Response 统一一下响应数据的结构,在实际项目中还会创建响应码枚举类型。

package com.to.jing.course.sdk.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Objects; /** * 定义接口返回的数据格式 <br/> * 主要包括 code msg data */ @Data @AllArgsConstructor @NoArgsConstructor public class Response { private Integer code; private String msg; private Object data; public static Response SUCCESS(Object data){ return new Response(0,"success",data); } public static Response SUCCESS(){ Response response = new Response(); response.setCode(0); response.setMsg("success"); return response; } public void failed(String msg){ this.code = -1; this.msg = msg; this.data = null; } }

创建 CachePassController.java,添加路由,这里使用了 @PathVariable 注解,可以直接从路由上获取参数。

package com.to.jing.course.server.controller; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import com.to.jing.course.sdk.domain.Response; import com.to.jing.course.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @Slf4j public class CachePassController { @Autowired private UserService userService; @RequestMapping(value = "/cache/pass/{id}" ,produces = "application/json;charset=UTF-8") public String cachePass(@PathVariable("id") Integer id){ //定义接口返回的数据格式 Response response = Response.SUCCESS(); try { response.setData(userService.getUserInfo(id)); }catch (Exception e){ response.failed("失败"+e.getMessage()); } return JSON.toJSONString(response, SerializerFeature.BrowserCompatible); } }

运行 app,在浏览器中输入 http://localhost:8080/cache/pass/1,可以看到结果如下图:
image.png
运行日志如下:
image.png
这是由于上一章节我们在表里创建了 id 为 1 的用户,下面我们访问一下 id 为 2,http://localhost:8080/cache/pass/2
image.png
运行日志:
image.png
再次访问 http://localhost:8080/cache/pass/2,运行日志为
image.png
从图中可以看到无效数据也被我们缓存到了 redis 中。这里更推荐小伙伴们使用 postman 测试接口,使用 anotherRedisMannager 可以更直观地看到 redis 里的键值。

image.png

源码地址

https://github.com/ToJing/spring-boot-course tagV2.0

博客地址

http://m.loveplaycat.club:8080/articles/2020/11/16/1605520428207.html

参考

  • 书籍《基于 Springboot 实现 Java 分布式中间件开发入门与实战》
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    946 引用 • 1460 回帖 • 1 关注
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    286 引用 • 248 回帖 • 14 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...