Redis简介

在传统的Java Web项目中存储数据,主要是用关系型数据库,如MySQL、SqlServer、Oracle等等,这些数据库的数据持久化在磁盘上,而磁盘的读写速度比较慢,而一般的管理系统上又不存在瞬间的高并发场景,没有瞬间的读写大量数据的要求,这个情况使用关系型数据库,把数据持久化在磁盘上没什么太大问题,但在互联网应用中瞬间的高并发和瞬间读写大量数据的场景比较多,就不是关系型数据库所能够承受的了,因此Java Web引入了NoSQL技术,它也是一种数据库,但基于内存而非磁盘,也提供了持久化的功能,因此读写速度远大于关系型数据,然而基于内存的数据库成本相对关系型数据库而言非常高,因此什么数据使用NoSQL,什么数据使用关系型数据库要进行清晰的规划才能得到更好地投入产出比

Redis就是当前比较广泛的NoSQL,其性能十分优越,可以支持每秒十几万次的读写操作,性能远超关系型数据库,并且支持集群、分布式、主从同步等配置,若不考虑成本,理论上可以无限扩展,让更多的数据存储在内存中,并且Redis还支持事务,这对高并发场景下的数据一致性特别重要

Redis的性能优越主要源于如下几个因素:

  • 它是基于ANSI C语言编写的,ANSI C语言是更接近于汇编语言的机器语言,运行速度非常快
  • 内存存储:Redis将所有数据存储在内存中,避免了磁盘I/O的延迟,使得数据访问速度极快。内存读写速度远远高于硬盘,这是Redis能够实现高速响应的基础
  • 单线程模型:Redis使用单线程来处理客户端的所有请求,避免了多线程切换上下文的开销。虽然单线程限制了它不能并行处理多个请求,但在没有锁竞争的情况下,它的处理效率非常高,尤其适合于执行大量简单命令的场景
  • 数据结构优化:Redis不仅仅是一个键值存储系统,它还支持多种数据结构如字符串、哈希、列表、集合、有序集合等,这些数据结构都经过精心设计和优化,能够在执行特定操作时达到很高的性能
  • I/O多路复用:Redis使用I/O多路复用技术(如epoll on Linux),可以同时监听多个套接字,当任何一个套接字准备好进行读写操作时,Redis就可以进行处理,这大大提高了处理并发连接的效率,无需为每个连接创建一个单独的线程
  • 持久化策略:Redis提供了RDB(快照)和AOF(追加文件)两种持久化方式,用户可以根据实际需求选择合适的策略,在保证数据安全的同时,尽量减少对性能的影响
  • 缓存淘汰策略:当内存不足时,Redis通过配置的缓存淘汰策略(如LRU、LFU等)自动移除不常访问的数据,确保热点数据始终被保留,提升访问效率

综上所述,Redis结合了内存存储、高效的单线程模型、优化的数据结构、I/O多路复用技术以及灵活的持久化和缓存淘汰策略,共同造就了其高速的数据处理能力,是关系型数据库的几倍到几十倍

然而缺点也比较明显,例如数据存储在内存中部分持久化在磁盘,持久化能力有限,因此是不完全安全的,遇上机器故障数据容易丢失,此外NoSQL的数据完整性、事务能力、可靠性及可扩展性都远不及关系型数据库,且也不支持复杂的计算远不如关系型数据库的SQL语句强大

Redis数据结构简介

Redis是一种基于内存的数据库,并且提供一定的持久化功能,它是一种键值数据库,使用key作为索引找到当前缓存的数据,并且返回给调用者,当前的Redis支持多种数据结构,例如字符串、列表、集合、哈希、有序集合、基数、数据流和地理空间索引等

使用Redis编程要了解其支持的数据结构及相关命令,此外这些数据结构,Redis不但提供了存储功能,还能对存储的数据进行计算,例如字符串支持浮点数的自增、自减、字符求子串;集合可以求并集、交集;有序集合可以排序等等

如下表所示是常用的Redis数据结构

互联网应用主流框架整合之Redis基础_数据

Redis和关系型数据库的差异

使用Redis的常见场景

  • 缓存:在对数据的读写操作中,实际上读操作的次数远超过写操作,通常会超过70%都是读操作,当发送SQL读取数据时,数据库会去磁盘把对应的数据索引回来,而索引磁盘是一个相对缓慢的过程;如果使用Redis则不需要读写磁盘,运行在内存中速度显然会快很多
    而磁盘的容量可以是TB级别十分廉价,而内存的容量一般达到几百GB就相当不错了,因此在使用Redis时,需要有条件的存储,通常要考虑数据是否常用、数据是否读操作较多、数据大小如何,通常情况下是用Redis做一级缓存,读操作直接读Redis,如果不存在则读数据库,拿到数据后写入Redis,并返回给前端,第二次再读的时候还是先读Redis,这样访问速度就大大提高了,且减轻了关系型数据库的负担,而写入操作则同步更新关系型数据和Redis即可
  • 高速读写:这种场景往往是异步写入关系型数据库,首先将热点数据事先存放到Redis,当高速读写完成后再将满足条件的数据向关系型数据库同步,这种场景往往是秒杀或者抢红包之类的场景,当商品数量为0或者红包金额为0或者请求超时时,则将Redis的数据向关系型数据库批量同步更新,从而完成持久化的工作,一般缓存不做持久化

在实际开发中更复杂一些,还需要考虑数据安全、数据一致性、限制流量、有效请求、无效请求、事务一致性等等维度

Redis的Java API

在Java中可以简易的使用Redis,也可以通过Spring的RedisTemplate使用Redis,还可以SpringBoot的注解方式使用Redis

Java中简单使用Redis

首先需要引入如下依赖

<!-- 引入Jedis依赖,用于Java操作Redis -->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>3.2.0</version>
    </dependency>

然后编写简单的测试代码如下所示

package com.sr.test;

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

public class TestJedis {

    public static void main(String[] args) {
        testJedis();
        testPool();
    }

    public static void testJedis() {
        // 连接Redis
        Jedis jedis = new Jedis("192.168.3.115", 6379);
        // jedis.auth("Ms123!@#");
        int i = 0; // 记录操作次数
        try {
            // 开始毫秒数
            long start = System.currentTimeMillis();
            while (true) {
                long end = System.currentTimeMillis();
                // 当大于等于1000毫秒(相当于1秒)时,结束操作
                if (end - start >= 1000) {
                    break;
                }
                i++;
                jedis.set("test" + i, i + "");
            }
        } finally {// 关闭连接
            jedis.close();
        }
        // 打印1秒内对Redis的操作次数
        System.out.println("redis每秒操作:" + i + "次");
    }

    public static void testPool() {
        JedisPoolConfig poolCfg = new JedisPoolConfig();
        // 最大空闲数
        poolCfg.setMaxIdle(50);
        // 最大连接数
        poolCfg.setMaxTotal(100);
        // 最大等待毫秒数
        poolCfg.setMaxWaitMillis(20000);
        // 使用配置创建连接池
        JedisPool pool = new JedisPool(poolCfg, "192.168.3.115");
        Jedis jedis = null;
        try {
            // 从连接池中获取单个连接
            jedis = pool.getResource();
            // 设置密码
            // jedis.auth("Ms123!@#");
            jedis.set("pool_key1", "pool_value1");
        } finally {// 关闭连接
            jedis.close();
        }
        // 关闭连接池
        pool.close();
    }
}

执行代码,控制台打印如下内容

....
redis每秒操作:33011次

Process finished with exit code 0

然后检查一下Redis,写入了(pool_key1", "pool_value1),代码中使用了两种链接redis的方式,第一种是用了一个独立的链接,第二种使用了连接池来管理链接,Redis的连接池提供了类redis.clients.jedis.JedisPool来创建Redis的连接池对象,然后使用redis.clients.jedis.JedisPoolConfig类对连接池进行配置

在Spring使用Redis

由于Redis只能提供基于字符串的操作,而Java以使用类对象为主,所以需要Redis存储的字符串和Java对象相互转换,例如一个角色对象,没办法直接存到Redis里需要进一步转换,Spring提供了序列器,通过这个序列器Spring可以将对象转换成字符串,Redis可以将其存起来,并且在读取的时候,通过反序列化再将字符串转换为Java对象,在Spring中使用Redis,需要引入如下依赖

<!-- Spring Data Redis -->
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-redis</artifactId>
			<version>2.2.4.RELEASE</version>
		</dependency>
		<!-- Jedis -->
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>3.2.0</version>
		</dependency>

这样就可以使用spring-data-redis操作Redis了,spring-data-redis内部提供了RedisTemplate,它通过连接工厂RedisConnectionFactory获取Redis连接。

首先要配置Redis的连接池,如下所示

package com.sr.config;

/****import****/

@Configuration
public class RedisConfig {

	@Bean("redisPoolConfig")
	public JedisPoolConfig poolConfig() {
		JedisPoolConfig poolCfg = new JedisPoolConfig();
		// 最大空闲数
		poolCfg.setMaxIdle(50);
		// 最大连接数
		poolCfg.setMaxTotal(100);
		// 最大等待毫秒数
		poolCfg.setMaxWaitMillis(20000);
		return poolCfg;
	}

有了连接池之后,就需要配置Redis的连接工厂,接口是RedisConnectionFactory,他有两个比较重要的实现类

互联网应用主流框架整合之Redis基础_数据库_02


然后在RedisConfig类下装配一个JedisConnectionFactory对象,代码如下所示

/**
     * 创建Jedis连接工厂
     * @param jedisPoolConfig
     * @return 连接工厂
     */
    @Bean("redisConnectionFactory")
    public RedisConnectionFactory redisConnectionFactory(@Autowired JedisPoolConfig jedisPoolConfig) {
        // 独立Jedis配置
        RedisStandaloneConfiguration rsc = new RedisStandaloneConfiguration();
        // 设置Redis服务器
        rsc.setHostName("192.168.80.128");
        // 如需要密码,设置密码
        rsc.setPassword("abcdefg");
        // 端口
        rsc.setPort(6379);
        // 获得默认的连接池构造器
        JedisClientConfigurationBuilder jpcb = JedisClientConfiguration.builder();
        // 设置Redis连接池
        jpcb.usePooling().poolConfig(jedisPoolConfig);
        // 获取构建器
        JedisClientConfiguration jedisClientConfiguration = jpcb.build();
        // 创建连接工厂
        return new JedisConnectionFactory(rsc, jedisClientConfiguration);
    }

这样就完成了一个RedisConnectionFactory的配置,这里配置的是JedisConnectionFactory,如果需要LettuceConnectionFactory,可以将代码中JedisConnectionFactory改为LettuceConnectionFactory,并且引入相关依赖即可

有了RedisConnectionFactory,就可以开始配置RedisTemplate<K,V>了,普通的连接没办法把Java对象直接存入Redis,需要开发来提供方案,通常是将对象序列化,然后使用Redis序列化存储对象,取回时,再通过反序列化将之前存储的内容转换为Java对象接口;RedisTemplate<K,V>中提供了封装方案,从RedisTemplate的定义中不难发现,它存在K和V两个泛型,因为Redis是一个Key-value数据库,需要将key和value序列化后才能存储,而为了便于序列化,Spring内部提供了RedisSerializer<T>接口和一些实现类来完成key和value与java对象之间序列化和反序列化的工作;

互联网应用主流框架整合之Redis基础_数据库_03


以下是RedisSerializer<T>接口实现类说明:

  • StringRedisSerializer 将Java对象序列化为字符串。这通常用于序列化字符串类型的键或值,是最基本的序列化方式之一。
  • JdkSerializationRedisSerializer 使用Java的序列化机制,即将对象转换为字节数组。这适用于任何实现了Serializable接口的Java对象,但是序列化后的结果可能较大,且不跨语言。
  • OxmSerializer 基于Spring的OXM(Object/XML Mappers)模块,可以将对象序列化为XML或其他格式。这在需要XML格式数据时很有用。
  • Jackson2JsonRedisSerializer 使用Jackson库将对象序列化为JSON格式。这提供了良好的可读性和跨语言兼容性,同时支持复杂的数据结构。
  • GenericJackson2JsonRedisSerializer 类似于Jackson2JsonRedisSerializer,但是更通用,可以序列化更广泛的Java类型到JSON。
  • GenericTostringSerializer 将对象转换为其toString()方法的结果,然后使用StringRedisSerializer序列化。这通常用于调试目的,而不是生产环境。
  • ByteArrayRedisSerializer 直接将对象视为字节数组进行序列化和反序列化,适用于原始二进制数据。

在Spring Data Redis中,你可以通过配置RedisTemplateStringRedisTemplatesetKeySerializer, setValueSerializer, setHashKeySerializer, 和 setHashValueSerializer方法来指定不同的序列化器,从而控制如何序列化和反序列化键和值。

这些序列化器的选择通常基于性能需求、数据类型、跨语言兼容性以及是否需要人类可读等因素。例如,如果性能是关键因素,你可能会选择ByteArrayRedisSerializerGenericJackson2JsonRedisSerializer,而如果数据主要是字符串,那么StringRedisSerializer可能是最合适的选择

Spring提供的RedisTemplate有几个关于设置序列化器的关键属性如下

@Nullable
    private RedisSerializer<?> defaultSerializer;
    @Nullable
    private RedisSerializer keySerializer = null;
    @Nullable
    private RedisSerializer valueSerializer = null;
    @Nullable
    private RedisSerializer hashKeySerializer = null;
    @Nullable
    private RedisSerializer hashValueSerializer = null;
/**
	 * 创建RedisTemplate 
	 * @param connectionFactory Redis连接工厂
	 * @return RedisTemplate对象
	 */
	@Bean("redisTemplate")
	public RedisTemplate<String, Object> redisTemplate(@Autowired RedisConnectionFactory connectionFactory) {
		// 创建RedisTemplate
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
		// 字符串和JDK序列化器
		StringRedisSerializer strSerializer = new StringRedisSerializer();
		JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
		// 设置键值序列化器
		redisTemplate.setKeySerializer(strSerializer);
		redisTemplate.setValueSerializer(jdkSerializer);
		// 设置哈希字段和值序列化器
		redisTemplate.setHashKeySerializer(strSerializer);
		redisTemplate.setHashValueSerializer(jdkSerializer);
		// 给RedisTemplate设置连接工厂
		redisTemplate.setConnectionFactory(connectionFactory);
		return redisTemplate;
	}

代码中使用StringRedisSerializer作为Redis的key的序列化器,使用JdkSerializationRedisSerializer 作为value的序列化器,这样就配置了一个RedisTemplate对象,然后spring-data-redis就知道用对应的序列化器转换Redis不同类型数据的键值了;更多的时候会使用字符串,字符串不但可读性高且Redis最基本的数据类型也是字符串,Spring提供了StringRedisTemplate对象,它继承了RedisTemplate,可以认为StringRedisTemplate对象是一个键值为String泛型的RedisTemplate专门针对字符串进行操作

/**
	 * 创建StringRedisTemplate
	 * @param connectionFactory 连接工厂
	 * @return StringRedisTemplate对象
	 */
	@Bean("stringRedisTemplate")
	public StringRedisTemplate  stringRedisTemplate(@Autowired RedisConnectionFactory connectionFactory) {
		// 创建StringRedisTemplate对象
		StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
		// 设置连接工厂
		stringRedisTemplate.setConnectionFactory(connectionFactory);
		return stringRedisTemplate;
	}

为了测试上述代码是否生效,先建一个可序列化的角色对象,代码如下

package com.sr.pojo;

import java.io.Serializable;

/**
 * 角色类,表示系统中的一个角色
 * 实现Serializable接口,使得角色对象可以被序列化
 */
public class Role implements Serializable {

    // 序列化版本ID,用于保证序列化兼容性
    private static final long serialVersionUID = 6977402643848374753L;

    // 角色ID,唯一标识一个角色
    private Long id;
    // 角色名称,表示角色的名称
    private String roleName;
    // 角色描述,对角色的详细描述
    private String note;

    /**
     * 默认构造函数,用于创建一个新的角色实例
     */
    public Role() {
    }

    /**
     * 构造函数,用于创建一个带有指定属性的角色实例
     * @param id    角色的ID
     * @param roleName 角色的名称
     * @param note  角色的描述
     */
    public Role(Long id, String roleName, String note) {
        this.id = id;
        this.roleName = roleName;
        this.note = note;
    }
    
    /**
     * 获取角色ID
     * @return 角色的ID
     */
    public Long getId() {
        return id;
    }

    /**
     * 设置角色ID
     * @param id 角色的ID
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * 获取角色名称
     * @return 角色的名称
     */
    public String getRoleName() {
        return roleName;
    }

    /**
     * 设置角色名称
     * @param roleName 角色的名称
     */
    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    /**
     * 获取角色描述
     * @return 角色的描述
     */
    public String getNote() {
        return note;
    }

    /**
     * 设置角色描述
     * @param note 角色的描述
     */
    public void setNote(String note) {
        this.note = note;
    }

}

要序列化对象,它必须实现Serializable接口,这样这个对象才能被序列化,serialVersionUID代表序列化的版本编号

接下来就可以测试对象的序列化和反序列化,代码如下

public static void testRedisTemplate() {
        // 创建IoC容器
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
        // 获取RedisTemplate,注意StringRedisTemplate是其子类,因此只能通过名称获取
        RedisTemplate<String, Object> redisTemplate = ctx.getBean("redisTemplate", RedisTemplate.class);
        // 创建Role对象
        Role role = new Role(1L, "role_name_1", "note_1");
        // 让Redis服务器存放对象
        redisTemplate.opsForValue().set("role-1", role);
        // 获取对象
        Role role1 = (Role) redisTemplate.opsForValue().get("role-1");
        System.out.println(role1.getRoleName());
        // 获取StringRedisTemplate
        StringRedisTemplate stringRedisTemplate = ctx.getBean(StringRedisTemplate.class);
        // 对Redis服务器的String的键值操作
        stringRedisTemplate.opsForValue().set("template-1", "value-1");
        String value = stringRedisTemplate.opsForValue().get("template-1");
        System.out.println(value);
    }

然后检查Redis里的数据,如下图所示

互联网应用主流框架整合之Redis基础_数据库_04


然而这里边有个问题,就是我们使用连接池来管理链接,并不能保证每次使用RedisTemplate都是同一个操作同一个Redis链接,比如如下代码

// 让Redis服务器存放对象
        redisTemplate.opsForValue().set("role-1", role);
        // 获取对象
        Role role1 = (Role) redisTemplate.opsForValue().get("role-1");

代码很简单,但是在内部他们是同一个连接池的不同链接,为了使所有操作都来自同一个链接,可以使用SessionCallback或者RedisCallback接口,其中RedisCallback是底层的封装,SessionCallback是相对高级的封装接口使用起来更为友好,通过这两个接口之一就可以把多个命令放入同一个Redis连接中执行,这样对于资源的损耗更小,如下代码所示

public static void testRedisTemplate2() {
        // 创建IoC容器
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
        // 获取RedisTemplate,注意StringRedisTemplate是其子类,因此只能通过名称获取
        RedisTemplate<String, Object> redisTemplate = ctx.getBean("redisTemplate", RedisTemplate.class);
        // Lambda表达式创建SessionCallback
        SessionCallback callBack1 = new SessionCallback() {
            @Override
            public Object execute(RedisOperations ops) throws DataAccessException {
                // 创建Role对象
                Role role = new Role(1L, "role_name_1", "note_1");
                ops.boundValueOps("role-1").set(role);
                Role role1 = (Role) ops.boundValueOps("role-1").get();
                return role1;
            }
        };
        redisTemplate.execute(callBack1);
        // 创建Role对象
        Role role = new Role(1L, "role_name_1", "note_1");
        // 让Redis服务器存放对象
        redisTemplate.opsForValue().set("role-1", role);
        // 获取对象
        Role role1 = (Role) redisTemplate.opsForValue().get("role-1");
        System.out.println(role1.getRoleName());
        // 获取StringRedisTemplate
        StringRedisTemplate stringRedisTemplate
                = ctx.getBean(StringRedisTemplate.class);
        // Lambda表达式创建SessionCallback
        SessionCallback callBack2 = new SessionCallback() {
            @Override
            public Object execute(RedisOperations ops) throws DataAccessException {
                ops.boundValueOps("template-1").set("value-1");
                String value1 = (String) ops.boundValueOps("template-1").get();
                return value1;
            }
        };
        String value = (String) stringRedisTemplate.execute(callBack2);
        System.out.println(value);
    }