Redis深入学习:Jedis和Spring的RedisTemplate

前言:在 JavaWeb 中实现对 Redis 的操作,主要有两种方式:Jedis和RedisTemplate。Jedis是Redis官方推荐的面向Java操作Redis的客户端开发Jar包;而RedisTemplate是Spring框架对Jedis API的进行了高度封装,支持连接池自动管理,我们可以在Spring应用中通过简单的连接池配置信息就能访问Redis服务并进行相关缓存操作。也就是Spring的spring-data-redis的相关Jar包,它还默认提供了两个使用Redis的类StringRedisTemplate和RedisTemplate,其中RedisTemplate可以支持Redis没有的缓存对象的操作,而StringRedisTemplate用来存储字符串。

一、Jedis的概述

1、什么是Jedis?

Jedis 是 Redis 官方推荐的Java客户端开发包,集成了Redis的命令操作,提供了连接池管理。通过Jedis我们可以实现连接Redis,以及操作 Redis 。简而言之,Jedis是Redis的Java连接开发工具。

redis-cli.exe是Redis官方提供的客户端,可以看作一个shell程序,它可以发送命令对redis进行操作;对于Jedis而言,同理是使用Java语言操作Redis,相当于Mysql驱动。Jedis提供了很多接口供Java语言调用。可以在Redis官网下载,当然还有一些开源爱好者提供的客户端,如Jredis、SRP等等。

Jedis常用API:

方法 解释
new Jedis(host, port) 创建jedis对象,参数host是redis服务器地址,参数port是redis服务端口
set(key,value) 设置字符串类型的数据
get(key) 获得字符串类型的数据
hset(key,field,value) 设置哈希类型的数据
hget(key,field) 获得哈希类型的数据
lpush(key,values) 设置列表类型的数据
lpop(key) 列表左面弹栈
rpop(key) 列表右面弹栈
del(key) 删除指定的key

2、Jedis 的使用

就像在学习 springmvc 框架之前学习 servlet 一样,了解 Jedis 的使用,看一下单机中 Jedis 的使用,

(1)Maven导入相关jar包

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>

(2)Jedis直连使用

Jedis直连,本质是定义一个tcp连接,然后使用socket技术进行通信。每次操作新创建一个Jedis对象,执行完毕后关闭连接释放对象,对应的就是一次tcp连接。

package test;


import com.hs.springbootdemo.SpringbootdemoApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import redis.clients.jedis.Jedis;

@RunWith(SpringRunner.class)//SpringBoot 2.X 默认使用Junit4
@SpringBootTest(classes = SpringbootdemoApplication.class)
public class JedisTest
{
    @Test
    public void testJedis()
    {
        //1、生成一个jedis对象,这个对象负责和指定Redis节点进行通信
        Jedis jedis = new Jedis("localhost", 6379);
        //带密码需要执行认证方法,这里我的Redis没有设密码就不用管
        //jedis.auth("123456");

        //2、jedis存入数据
        jedis.set("hello", "world");
        //3、jedis获取数据
        String value = jedis.get("hello");

        Logger logger = LoggerFactory.getLogger(JedisTest.class);
        logger.warn("从Redis中存取数据:"+value);
    }
}

测试结果:直接使用Jedis很简单

Redis 中最主要的就是读写数据。Redis 只能支持六种数据类型(string/hash/list/set/zset/hyperloglog)的操作,但在 Java 中我们却通常以类对象为主,所以在需要 Redis 存储的五中数据类型与 Java 对象之间进行转换,如果自己编写一些工具类,比如一个角色对象的转换,还是比较容易的,但是涉及到许多对象的时候,这其中无论工作量还是工作难度都是很大的,所以总体来说,就操作对象而言,使用 Redis 还是挺难的,好在 Spring 框架对这些进行了封装和支持。

上面说到了 Jedis 无法操作对象的问题,无法在那些基础类型和 Java 对象之间方便的转换,但是在 Spring 应用中,这些问题都可以通过使用RedisTemplate得到解决!

(3)Jedis直连的缺陷

首先我们如果每次使用缓存都生成一个 Jedis 对象的话,这样意味着会建立很多 socket 连接,造成系统资源浪费;同时Jedis在实现上是直连redis server,多线程环境下非线程安全,除非使用连接池,为每个Jedis实例增加物理连接。

因此,为了避免这些问题,引入了池的概念 JedisPool。JedisPool 是一个线程安全的网络连接池,预先生成一批jedis连接对象放入连接池中,当需要对redis进行操作时从连接池中借用jedis对象,操作完成后归还。这样jedis对象可以重复使用,避免了频繁创建socket连接,节省了连接开销。所以就可以有效的解决以上问题以实现系统的高性能。

我们可以把JedisPool理解成项目中的数据库连接池,例如:阿里巴巴的 druid~

3、JedisPool连接池使用

Jedis连接资源的创建与销毁是很消耗程序性能,所以Jedis为我们提供了Jedis的池化技术。

JedisPool在创建时初始化一些连接资源存储到连接池中,使用Jedis连接资源时不需要创建,而是从连接池中获取一个资源进行Redis的操作,使用完毕后,不需要销毁该Jedis连接资源,而是将该资源归还给连接池,供其他请求使用。这样jedis对象可以重复使用,避免了频繁创建socket连接,节省了连接开销。

Jedis直连和使用连接池JedisPool的对比:

  优点 缺点
直连 简单方便适用于少量长期连接的场景 存在每次新建/关闭TCP开销,资源无法控制,存在连接泄露的可能,Jedis对象线程不安全
连接池 Jedis预先生成,降低开销,连接池的形式保护和控制资源的使用 相对于直连,使用相对麻烦,尤其在资源管理上需要很多参数来保证,一旦规划不合理也会出现问题。

(1)JedisPool简单使用

这里只是对连接池进行一个简单使用,实际开发通常会对JedisPool进行封装,进行一些参数配置和方法定义等

package test;

import com.hs.springbootdemo.SpringbootdemoApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 *JedisPool类:因为连接池中会有很多jedis实例,RedisPool对象会很大,所以我们需要把他写成单例模式,
 * 如果是交由Spring管理就不用了,因为Spring管理的Bean默认是单例的。
 */

@RunWith(SpringRunner.class)//SpringBoot 2.X 默认使用Junit4
@SpringBootTest(classes = SpringbootdemoApplication.class)
public class JedisPoolTest
{
    private Logger logger = LoggerFactory.getLogger(JedisTest.class);//logback日志工具

    @Test
    public void testJedisPool()
    {
        //1、获得连接池配置对象,设置配置项
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(30); // 最大连接数
        config.setMaxIdle(10);  // 最大空闲连接数
        config.setMaxWaitMillis(10*1000); // 最大等待时间

        //2、初始化Jedis连接池,通常来讲JedisPool应该是单例的
        JedisPool jedisPool = new JedisPool(config,"localhost",6379);

        Jedis jedis=null;
        try {
            //从连接池获取jedis核心对象
            jedis = jedisPool.getResource();
            //设置数据
            jedis.set("name","hs");
            //获得数据
            String str = jedis.get("name");
            logger.warn("RedisPool使用:"+str);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            if(jedis!=null){
                jedis.close();//关闭连接,释放资源
            }
        }

        //虚拟机关闭时,释放pool资源
        if(jedisPool!=null){
            jedisPool.close();
        }
    }
}

测试结果:

(2)JedisPool 属性配置(JedisPoolConfig)

对于企业级开发来说,连接池的合理使用是非常重要的,如果设置不当会引起很多不必要的麻烦,容易造成线上的故障。

为了方便使用,Jedis提供了JedisPoolConfig,它本身继承了GenericObjectPoolConfig设置了一些空闲监测设置

JedisPool的配置参数大部分是由JedisPoolConfig的对应项来赋值的。

具体如何正确设置,可以置参考博客:Jedis连接池的使用及配置优化

(3)JedisPool封装工具类例子

JedisPoolUtil类:封装Redis连接池配置JedisPoolConfig信息,通过单例RedisPool获取redis对象

因为连接池中会有很多jedis实例,RedisPool对象会很大,所以我们需要把他写成单例模式,如果是交由Spring管理就不用了,因为Spring管理的Bean默认是单例的。

a、JedisPoolUtil类

package test;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Redis 连接池工具包:封装Jedis连接池配置JedisPoolConfig信息,通过单例RedisPool获取redis对象
 */
public class JedisPoolUtil
{
   //下面的这些配置属性可以根据需要修改。其实一般通过读取jedis.properties配置文件指定比较方便,这里只是作封装例子
    private static final String HOST = "132.232.6.208";
    private static final int PORT = 6379;
    private static final int MAX_TOTAL = 100;
    private static final int MAX_IDEL = 100;
    private static final int MAX_WAITMILLIS = 10 * 1000;
    private static volatile JedisPool jedisPool = null;
    private JedisPoolUtil() {
    }

    /**
     * 1、获取RedisPool单例模式的对象:单例模式指的是在应用整个生命周期内只能存在一个实例
     *
     * @return RedisPool实例(单例)
     */
    public static JedisPool getJedisPoolInstance()
    {
        if (jedisPool == null)
        {
            synchronized (JedisPoolUtil.class)
            {
                if (jedisPool == null) {

                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    poolConfig.setMaxTotal(MAX_TOTAL);           // 最大连接数
                    poolConfig.setMaxIdle(MAX_IDEL);              // 最大空闲连接数
                    poolConfig.setMaxWaitMillis(MAX_WAITMILLIS);  // 最大等待时间
                    poolConfig.setTestOnBorrow(true);       // 检查连接可用性, 确保获取的redis实例可用

                    jedisPool = new JedisPool(poolConfig, HOST, PORT);
                }
            }
        }

        return jedisPool;
    }

    /**
     * 2、从连接池中获取一个 Jedis 实例(连接)
     *
     * @return Jedis 实例
     */
    public static Jedis getJedisInstance() {

        return getJedisPoolInstance().getResource();
    }

    /**
     * 3、将Jedis对象(连接)归还连接池
     *
     * @param jedis     连接对象
     */
    public static void releaseJeids(Jedis jedis) {

        if (jedis != null) {
            jedis.close();// jedisPool.returnResourceObject(jedis)已废弃
        }
    }

}

b、编写测试代码:

package test;

import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class JedisPoolUtilTest {

    JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();

    // 测试单例
    @Test
    public void test1() {
        JedisPool A = JedisPoolUtil.getJedisPoolInstance();
        JedisPool B = JedisPoolUtil.getJedisPoolInstance();

        System.out.println(A == B);
    }

    @Test
    public void test2() {
        Jedis jedis = null;

        try {
            jedis = jedisPool.getResource();  // 获取Redis连接对象

            // 业务
            jedis.set("key1", "value111");
            System.out.println(jedis.get("key1"));
        } finally {
            jedis.close(); // 关闭redis连接
        }
    }
}

运行结果:获取JedisPool的对象是单例模式的,然后从JedisPool连接池里面取出jedis对象进行缓存存取操作

4、项目使用的JedisUtil工具类

JedisPool一般通过读取jedis.properties配置文件指定JedisPoolConfig信息比较方便,这样需求变动时只需要修改配置文件,而不用修改代码。

(1)在项目Resource目录下新建jedis.properties配置文件如下

jedis.properties配置文件:

#redis服务器地址
host=127.0.0.1
#redis端口
port=6379
#最大连接数
maxtotal=100
#最大空闲连接数
maxidel=32
#最大等待时间10秒
maxwaitmillis=10000

(2)Jedis封装工具类

package test;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

        import java.util.ResourceBundle;

/**
  *  Jedis工具类:从配置文件jedis.properties中读取JedisPoolConfig信息生成JedisPool,然后从连接池获取jedis对象进行缓存存取操作
 */

public final class JedisUtil {

    private static String host;
    private static int port;
    private static int maxtotal;
    private static int maxidel;
    private static int maxwaitmillis;
    private JedisUtil() {}
    private static volatile JedisPool jedisPool = null;


    /**
     *  static静态块的代码主要用于类的初始化。在虚拟机加载类的时候就会加载执行,而且只执行一次,会在项目main函数之前执行。
     *  1、读取jedis.properties配置文件
     */
    static{
        ResourceBundle rb = ResourceBundle.getBundle("jedis");
        host = rb.getString("host");
        port = Integer.parseInt(rb.getString("port"));
        maxtotal = Integer.parseInt(rb.getString("maxtotal"));
        maxidel = Integer.parseInt(rb.getString("maxidel"));
        maxwaitmillis = Integer.parseInt(rb.getString("maxwaitmillis"));
    }

    /**
     * 一个类中可以定义多个静态代码块,按顺序执行
    *  2、创建单例连接池
    */
    static{
        if (jedisPool == null)
        {
            synchronized (JedisPoolUtil.class)
            {
                if (jedisPool == null) {

                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    poolConfig.setMaxTotal(maxtotal);           // 最大连接数
                    poolConfig.setMaxIdle(maxidel);              // 最大空闲连接数
                    poolConfig.setMaxWaitMillis(maxwaitmillis);  // 最大等待时间
                    poolConfig.setTestOnBorrow(true);       // 检查连接可用性, 确保获取的redis实例可用

                    jedisPool = new JedisPool(poolConfig,host,port);
                }
            }
        }
    }

    /**
     * 3、获取jedis
     */
    public static Jedis getJedis(){
        return jedisPool.getResource();
    }

    /**
    ** 4、将Jedis对象(连接)归还连接池
     */
    public static void close(Jedis jedis){
        if(jedis!=null){
            jedis.close();
        }
    }
}

(3)JedisUtil测试代码

package test;

import org.junit.Test;
import redis.clients.jedis.Jedis;

public class JedisUtilTest
{
    @Test
    public void test()
    {
        Jedis jedis = null;
        try {

            jedis = JedisUtil.getJedis(); // 获取Redis连接对象

            // 业务
            jedis.set("key1", "value111");
            System.out.println(jedis.get("key1"));

        } finally {
           JedisUtil.close(jedis); // 归还redis连接给JedisPool
        }
    }
}

测试结果:

二、Redis、Jedis和Spring Data Redis的区别

1、Redis

redis是一款开源的高性能Key-Value数据库,运行在内存中,由ANSI C编写。企业开发通常采用Redis来实现缓存。同类的产品还有memcache 、memcached 、MongoDB等。

2、Jedis

Jedis是Redis官方推出的一款面向Java的客户端,提供了很多接口供Java语言调用。可以在Redis官网下载,当然还有一些开源爱好者提供的客户端,如Jredis、SRP等等。

3、Spring Data Redis

Spring-data-redis是Spring大家族的一部分,提供了在srping应用中通过简单的连接池配置访问redis服务,对Reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装。其中的RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。而且RedisTemplate还支持对象缓存操作。

spring-data-redis针对Jedis 提供了如下功能:

    a.连接池自动管理,提供了一个高度封装的“RedisTemplate”类

    b.针对Jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口

  •  ValueOperations:简单K-V操作

  •  SetOperations:set类型数据操作

  •   ZSetOperations:zset类型数据操作

  •   HashOperations:针对map类型的数据操作

  •    ListOperations:针对list类型的数据操作

三、SpringBoot中使用RedisTemplate

第一部分我们对Jedis的使用进行了介绍,而且封装了一个JedisPoolUtil工具类。可是这样还是很麻烦,难道每次我们使用的时候都得复制这个JedisUtill 连接池配置工具类到新项目中?而且Jedis还不支持缓存对象的操作。程序员从来不需要重复遭轮子,Spring框架已经帮我们封装好了这一切!

Spring框架中的spring-data-redis模块对Jedis API的进行了高度封装,提供了在Spring应用中通过简单的连接池信息配置就可以访问Redis服务并进行相关缓存操作。SpringDataRedis相对于Jedis来说可以方便地更换Redis的Java客户端如多线程安全的Lettuce,比Jedis多了自动管理连接池的特性,不需要我们自己的JediPoolUtil封装工具类。

它还默认提供了两个使用Redis的类StringRedisTemplate和RedisTemplate,其中RedisTemplate可以支持Redis没有的缓存对象的操作,而StringRedisTemplate用来存储字符串。(其实它们都是RedisTemplate<K, V>泛型接口的实现类,我们可以自定义模板然后@AutoWired注入IOC容器中使用)

1、pom.xml添加redis的起步依赖

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

2、通过自带的StringRedisTemplate类存储对象到Redis中

package test;
 
import com.hs.springbootdemo.SpringbootdemoApplication;
import com.hs.springbootdemo.dao.entity.UserEntity;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
 
import javax.annotation.Resource;
 
@RunWith(SpringRunner.class)//SpringBoot 2.X 默认使用Junit4
@SpringBootTest(classes = SpringbootdemoApplication.class)
public class RedisTest {
 
    //如果无法注入RedisTemplate,就使用@Resource试试
    @Autowired
    private StringRedisTemplate stringRedisTemplate;//自带的字符串模板类,用于存储字符串
 
    @Autowired
    private RedisTemplate redisTemplate;//自带的对象模板类,用于存储对象
 
    @Test
    public void test() throws Exception
    {
        // 保存字符串
        stringRedisTemplate.opsForValue().set("username", "redis!!!");
        Logger logger = LoggerFactory.getLogger(RedisTest.class);
        String str = stringRedisTemplate.opsForValue().get("username");
        logger.warn(str);
    }
 
    @Test
    public void test1() throws Exception {
 
        UserEntity user = new UserEntity();
        user.setUsername("hello");
        user.setPassword("12345");
 
        redisTemplate.opsForValue().set("user_1", user);
        UserEntity user1 = (UserEntity) redisTemplate.opsForValue().get("user_1");
 
        System.out.println(user1.getUsername());
    }
}

SpringBoot 2.0已经使用Lettuce代替Jedis

其实,随着Spring Boot2.x的到来,支持的组件越来越丰富,也越来越成熟,其中对Redis的支持不仅仅是丰富了它的API,更是替换掉底层Jedis的依赖,取而代之换成了Lettuce高级Redis客户端,用于多线程安全同步,异步和响应使用。

LettuceJedis的都是连接Redis Server的客户端程序。Jedis在实现上是直连redis server,多线程环境下非线程安全,除非使用连接池JedisPool,为每个Jedis实例增加物理连接。Lettuce基于Netty的连接实例(StatefulRedisConnection),可以在多个线程间并发访问,且线程安全,满足多线程环境下的并发访问,同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。

为了多线程安全,以前是Jedis+JedisPool组合 ,现在在SpringBoot 2.0应用中直接使用Lettuce客户端的API封装RedisTemplate即可只要配置好连接池属性,那么SpringBoot就能自动管理连接池。

猜你喜欢

转载自blog.csdn.net/CSDN2497242041/article/details/102675435