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

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

技术选用

整合 Redis

在父模块定义版本

<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>
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
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

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 使用中常见的问题有缓存穿透、缓存雪崩、缓存击穿、数据一致性的问题。

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

首先,我们创建一个 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

参考

  • Spring

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

    828 引用 • 1392 回帖 • 665 关注
  • Redis

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

    217 引用 • 237 回帖 • 669 关注

赞助商 我要投放

欢迎来到这里!

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

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