Redis 技术简介

Redis(Remote Dictionary Server)是一个典型的非关系型数据库,它是开源的内存键值存储系统,也被称为数据结构服务器,支持多种数据结构(如字符串、哈希表、列表、集合、有序集合等),并提供了丰富的操作命令,可以对这些数据结构进行快速读写操作。

NoSQL 简介

主流的数据库都是关系型数据库,每次操作关系型数据库时都是 I/O 操作,I/O 操作时影响程序执行性能的主要原因之一,连接数据库和关闭数据库都是消耗性能的过程。尽量减少对数据库的连接操作,可以明显提升运行效率。

针对上面的问题,市场上出现了 NoSQL(Not Only SQL)数据库,意思是「不仅仅可以使用关系型数据库」。

常见的 NoSQL 数据库有:

  • memcached:键值对,内存型数据库,所有数据都在内存中。
  • Redis:和 memcached 类似,还具备持久化的能力。
  • HBase:以列作为存储。
  • MongoDB:以 Document 存储。

Redis 简介

Redis 是以 Key-Value 形式存储的 NoSQL 数据库。

它是使用 C 语言编写的。

Redis 的一般操作都在内存中,读写速度极快,所以常用作缓存工具使用。

Redis 也可以用作数据中心或消息队列。

Redis 以 solt(槽)作为数据存储单元,每个槽可以存储 N 个键值对,Redis 中固定有 16384 个槽。每个向 Redis 存储数据的 key 都会进行 crc16 算法后得出一个值后对 16384 取余,存放到这个结果队以哦那个的 solt。

通过 Redis Sentinel 提供高可用,通过 Redis Cluster 提供自动分区。

Redis 单机版安装和启动

Redis 的安装

  1. 安装 C 环境

    yum install -y gcc-c++ automake autoconf libtool make tcl
    
  2. 上传并解压 redis-xxx.tar.gz

    tar zxvf redis-7.0.11.tar.gz
    
  3. 编译并安装

    make && make install PREFIX=/opt/redis
    
  4. 拷贝配置文件到安装目录下

    cp redis.conf /opt/redis/bin/
    

Redis 的启动

服务器启动

  1. 启动 Redis:在 /opt/redis/bin 下执行

    ./redis-server
    

    这种启动方式会阻塞窗口,如果不希望终端被阻塞,可以修改配置文件:

    daemonize yes
    

    根据配置文件启动

    ./redis-server redis.conf
    

    就是非阻塞的了。

  2. 为了让其他地址也可以访问 redis,我们还可以在配置文件下修改:

    # 取消绑定
    # bind 127.0.0.1 -::1
    
    # 关闭保护模式
    protected-mode no
    
  3. 根据配置文件启动:

    ./redis-server redis.conf
    

客户端启动

在 /opt/redis/bin 下执行

./redis-cli

Redis 常用的五大类型

Redis 不仅仅支持简单的 K-V 类型的数据,同时还提供 list、set、zset、hash 等数据结构的存储,它还支持数据的备份,即 master-slave 模式的数据备份,同时 Redis 支持数据的持久化,可以将内存中的数据保持在磁盘上,重启的时候可以再次加载进行使用。

Redis 支持的五大数据类型包括 String、Hash、List、Set、ZSet。

String 字符串

String 是 redis 最基本的类型,和 memcache 相同,一个 key 对应一个 value。String 类型是二进制安全的,redis 的 String 可以包含任何数据,比如 jpg 图片或序列化对象,最大能 512MB 的数据。

Hash 哈希

Redis Hash 是一个键值对集合。它是一个 String 类型的 field 和 value 的映射表,特别适合存储对象。

Redis Hash

例如:需要存储这样的信息:用户 ID → 姓名、年龄、生日。

如果使用 String 存储,要么将信息封装成一个对象存储:

  • set u001 "张三,18,20010101":耗费了序列化和反序列化的事件

要么分成多个键值对:

  • mset user:001:name "张三" user:001:age18 user:001:birthday "20010101":又浪费了空间(多次存储 ID)

使用 Hash 就可以很好地解决这个问题。

List 列表

Redis 列表是简单的字符串列表,按照插入顺序排序,允许添加一个元素到列表的头部或者尾部。

List 的应用场景非常丰富,也是 Redis 的最重要的数据结构之一,我们可以使用 List 轻松实现最新消息排行等功能,或者实现消息队列(利用 List 的 PUSH 操作将任务存在 List 中,然后工作线程再用 POP 操作将任务取出进行执行)

Set 集合

Set 是字符串的无序集合。可以方便地实现交、并、补的实现,本质是一个 value 为 null 的哈希表。

利用 Set 的唯一性,可以很方便地统计出访问的所有 IP,或是共同好友。

Zset 有序集合

Zset 和 set 一样,也是 String 类型元素的集合,不同的是 zset 中每个元素会关联一个 double 类型的分数,通过分数来为集合中的成员进行从小到大的排序。

zset 是一种比较复杂的数据结构,使用的场景不算太多,适合带有权重的集合元素,例如一个游戏的得分排行榜。

Redis 常用命令

Redis 的帮助文档十分丰富,在网络上很容易找到,下面是其中一个:http://doc.redisfans.com/,下面介绍一些基本命令。

Key 操作

exists

判断 key 是否存在。

exists [key1] [key2] ...

返回值:返回存在的个数,不存在返回 0

expire

设置 key 的过期时间,单位秒

expire key [过期时间]

返回值:成功返回 1,失败返回 0

ttl

查看 key 的剩余过期时间

ttl [key]

返回值:返回剩余时间,单位秒,如果永不过期返回 -1,如果已经失效返回 -2

del

删除 key

del [key1] [key2] ...

返回值:被删除的 key 的数量

String 操作

set

设置键值对。

set [key] [value] 

返回值:成功 OK

get

获取值。

get [key]

返回值:key 对应的 value 值,不存在时返回 nil

setnx

当且仅当 key 不存在时才新增。

setnx [key] [value]

返回值:新增的键值对个数,key 已存在返回 0

setex

设置 key 的键值对和存活时间,无论是否存在指定 key 都能新增,如果存在 key 就覆盖旧值,同时必须指定过期时间。

setex [key] [ttl] [value]

返回值:OK

Hash 操作

Hash 类型的值中包含多组 field-value,相当于一个 string 类型的 field 和 value 的映射表:

Redis Hash

hset

给 key 中 field 设置值。

hset [key] [field] [value]

返回值:成功返回 1,失败返回 0

hget

获取 hash 中的 field 值。

hget [key] [field]

返回值:field 对应的 value,不存在返回 nil

hmset

一次设置 key 中多个 field 值。

hmset [key] [field1] [value1] [field2] [value2] ...

返回值:成功 OK

hmget

一次获取 key 中多个 field 值。

hmget [key] [field1] [field2] ...

返回值:value 列表

hvals

获取 key 中所有 field 的值。

hvals [key]

返回值:value 列表

hgetall

获取所有的 field 和 value。

hgetall [key]

返回值:field 和 value 的交替列表。

hdel

删除 key 中的若干个 field。

hdel [key] [field1] [field2] ...

返回值:成功删除 field 的数量

注意区分:del [key] 会删除整个哈希表。

List 操作

lpush / rpush

从列表左/右插入。

lpush/rpush [key] [value1] [value2] ...

返回值:插入后列表的元素个数

lpop / rpop

删除并获取表头/尾。

lpop/rpop [key]

返回值:删除的表头/尾的 value。

lrange

获取指定区间的元素值(闭区间),-1 表示最后一个成员,-2 表示倒数第二个,以此类推。

lrange [key] [left] [right]

返回值:查询到的 value 列表

llen

获取列表的长度。

llen [key]

返回值:列表的长度。

lrem

删除 count 个值为 value 的元素,count 为正,从左向右搜索;count 为负,从右向左搜索;count 为 0,删除所有值为 value 的元素。

lrem [key] [count] [value]

返回值:删除数量

Set 操作

sadd

向集合中添加内容。

sadd [key] [member1] [member2]...

返回值:添加后,集合的长度

srem

移除一个或多个成员。

srem [key] [member1] [member2] ...

返回值:成功移除的元素的数量。

scard

返回集合元素数量

scard [key]

返回值:集合长度

smembers

返回集合所有元素

smembers [key]

返回值:集合全部 member 列表

Zset 操作

zadd

向有序集合中添加内容。

zadd [key] [score1] [member1] [score2] [member2]...

返回值:新添加成功的个数,不包括被更新的成员

zrange

返回有序集合中,指定区间内的成员(按照分数升序排名),-1 表示最后一个成员,-2 表示倒数第二个,以此类推。

zrange [key] [left] [right] [withscores]

返回值:有序集合的 member 列表;如果添加 withscores,还会将分数也列出。

Redis 的持久化策略

Redis 不仅仅是一个内存型数据库,还具备持久化能力。

RDB

默认的模式。在指定时间间隔内生成数据快照,默认保存在 dump.rdb 文件中,当 redis 重启后,会自动加载 dump.rdb 中的内容到内存中。

可以采用 SAVE(同步,阻塞等待保存)或 BGSAVE(异步,在后台执行保存)手动保存数据。

可以设置服务器配置的 SAVE 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令,可以通过 save 选项设置多个保存条件,只要其中一个被满足,服务器就会自动执行 BGSAVE 命令。例如:

save 900 1
save 300 10
save 60 10000

只要满足三个条件中的任意一个,BGSAVE 就会被执行:

  • 服务器在 900 秒内,对数据库进行了至少 1 次修改。
  • 服务器在 300 秒内,对数据库进行了至少 10 次修改。
  • 服务器在 60 秒内,对数据库进行了至少 10000 次修改。

优点

  • rdb 文件时单独的一个紧凑文件,直接使用 rdb 文件就可以还原数据
  • 数据保存由子进程实现,不会影响父进程。
  • 恢复数据的效率高于 aof

缺点

  • 保存点之间如果 redis 意外关闭,将会丢失数据
  • 每次保存数据都需要 fork 子进程,数据量较大时,可能比较耗费性能

AOF

默认关闭,需要在配置文件中启动:

# 修改为 yes
appendonly yes
# aof 文件名
appendfilename "appendonly.aof"

AOF 的原理是监听执行的命令,如果发现执行了修改数据的操作,同时直接同步到数据库文件中。

Redis 支持 AOF 和 RDB 同时生效,如果同时生效,AOF 优先级高于 RDB(重新启动时优先使用 AOF 进行数据恢复)。

优点

  • 相对 RDB 数据更安全。

缺点

  • 占用空间更大
  • 速度更慢

Redis 主从复制

Redis 支持集群功能,为了保证单一结点的可用性,redis 支持主从复制功能。每个结点有 N 个复制品(replica),其中一个复制品是主(master),另外 N-1 个复制品是从(slave),也就是说 Redis 支持一主多从。

主从模式

主从的优点

  • 增加单一结点的健壮性,从而提升整个集群的稳定性。(当 Redis 中超过 1/2 结点不可用时,整个集群不可用)
  • 从节点可以对主节点数据备份,提升容灾能力
  • 读写分离:主节点一般用作写(同时具备读能力),从结点只能读,利用这个特性实现读写分离,写用主,读用从。

搭建主从集群

  1. 先关闭 redis 单机版

    ./redis-cli shutdown
    
  2. 新建目录

    mkdir /opt/replica
    
  3. 把之前安装的 redis 单机版的 bin 目录复制三份,分别命名为 master、slave1、slave2

    cp -r /opt/redis/bin /opt/replica/master
    cp -r /opt/redis/bin /opt/replica/slave1
    cp -r /opt/redis/bin /opt/replica/slave2
    
  4. 由于我们用一台机器模拟集群,为了避免端口冲突,需要在从节点配置文件中修改端口号并配置复制:

    # 修改端口号
    port 6380
    # 根据这个注释配置复制结点
    # replicaof <masterip> <masterport>
    replicaof 192.168.80.130 6379
    

    类似地,修改 slave2 的端口号和复制配置。

  5. 为了方便,我们可以在 replica 文件夹根目录下写一个脚本 startup.sh:

    ./master/redis-server master/redis.conf
    ./slave1/redis-server slave1/redis.conf
    ./slave2/redis-server slave2/redis.conf
    

    启动脚本,一次启动这个集群:

    sh startup.sh
    

    可以看到,三台 redis 服务器都已经启动:

    image-20230607110829436
  6. 在 master 服务器的客户端,使用

    info replication
    

    可以看到主从信息:

    image-20230607111045173

    尝试在主节点写入数据:

    image-20230607111221930

    在从节点也可以取得(注意需要使用 ./redis-cli -p [从节点端口号]才能进入从节点的客户端):

    image-20230607111327072

    从节点具备写的能力吗?

哨兵(Sentinel)

在 Redis 主从默认是只有主具备写的能力,从只负责读。如果主宕机,整个结点就不具备写的能力。但是如果让一个从变成主,整个节点又能继续工作,即使之前的主恢复过来,只需要当这个新主的从即可。

Redis 的哨兵就是帮助监控整个节点的,当主节点宕机等的情况下,帮助重新选取主。

Redis 中的哨兵支持单哨兵和多哨兵,单哨兵是只要它发现主宕机了,就直接选取另一个 master;多哨兵是根据我们设定,达到多少数量的哨兵认为 master 宕机之后才会进行重新选取主。

没有哨兵的情况

使用 kill 杀掉 master 进程,再进入从进程,仍然不具备写的能力。

image-20230607112318741

搭建多哨兵的具体步骤

  1. 新建目录

    mkdir /opt/sentinel
    
  2. 复制 Redis 到哨兵目录下

    cp -r /opt//redis/bin/* sentinel/
    
  3. 在 Redis 的解压目录下,有哨兵的配置文件 sentinel.conf,把它复制到哨兵目录下:

    cd /tmp/redis-7.0.11/
    cp sentinel.conf /opt/sentinel/
    
  4. 修改哨兵配置文件:

    # 后台启动
    daemonize yes
    # 日志存储位置,这里是因为这个哨兵的端口号是 26379 所以这样命名
    logfile "/opt/sentinel/26379.log"
    # 配置哨兵监听,选择主节点,2 代表配置多哨兵,只有 2 个哨兵都认为 master 宕机才会重新选主
    sentinel monitor mymaster 192.168.80.130 6379 2
    
  5. 复制哨兵文件,命名为「sentinel-26380.conf」,修改配置如下:

    port 26380
    logfile "/opt/sentinel/26380.log"
    

    同样地,再创建一个命名为「sentinel-26381.conf」的配置并修改。

  6. 启动 Redis 主从

    cd /opt/replica
    sh startup.sh
    
  7. 启动哨兵

    cd /opt/sentinel/
    ./redis-sentinel sentinel.conf
    ./redis-sentinel sentinel-26380.conf
    ./redis-sentinel sentinel-26381.conf
    
  8. 此时的状态:

    image-20230608105019207
    image-20230608105302916

    主进程是 master,如果杀掉主进程:

    kill -9 2927
    

    此时再进入 6380 端口的原 Slave 客户端:

    image-20230608105547599

    发现它已经变为主,既可以写,也可以读。

  9. 再次开启 master 下的 redis 服务:

    ./redis-server redis.conf
    ./redis-cli
    

    发现 master 恢复后依旧作为从,并且主是之前上位的 6380。

    image-20230608105915385
  10. 查看日志

    image-20230608110218400

集群(Cluster)

在集群中超过或等于 1/2 节点不可用时,整个集群不可用。为了搭建稳定的集群,一般采用奇数节点。

  1. 复制 redis 配置文件

    cp /opt/redis/bin/redis.conf /opt/redis/bin/redis-7001,conf
    
  2. 修改 redis-7001.conf

    port 7001
    pidfile /var/run/redis_7001.pid
    # 集群设置
    cluster-enabled yes
    cluster-config-file nodes-7001.conf
    cluster-node-timeout 15000
    

    类似地,创建 redis-7002.conf、redis-7003.conf、redis-7004.conf、redis-7005.conf

  3. 启动这些 redis 服务:

    注意:在启动前,删除之前的 dump.rdb

    创建启动脚本:

    ./redis-server redis-7001.conf
    ./redis-server redis-7002.conf
    ./redis-server redis-7003.conf
    ./redis-server redis-7004.conf
    ./redis-server redis-7005.conf
    ./redis-server redis-7006.conf
    

    启动:

    chmod u+x startup.sh
    ./startup.sh
    

    类似地,可以写一个终止脚本 stop.sh

    ./redis-cli -p 7001 shutdown
    ./redis-cli -p 7002 shutdown
    ./redis-cli -p 7003 shutdown
    ./redis-cli -p 7004 shutdown
    ./redis-cli -p 7005 shutdown
    ./redis-cli -p 7006 shutdown
    
  4. 注意:初次创建时,除了打开服务器,还必须配置集群才能让集群生效!

    在 redis3 时,需要借助 ruby 脚本实现集群,现在,可以使用 redis-cli 实现集群,更加方便。

    建议设置静态 ip,ip 改变集群生效。

     ./redis-cli --cluster create \
    192.168.80.130:7001 \
    192.168.80.130:7002 \
    192.168.80.130:7003 \
    192.168.80.130:7004 \
    192.168.80.130:7005 \
    192.168.80.130:7006 \
    --cluster-replicas 1
    

    这里的含义是:创建这六个地址的集群,每个集群节点有一个复制(一主一从)。

  5. 测试集群时,记得最后一个 -c 参数

    ./redis-cli -p 7001 -c
    

    可见,集群已经生效,其他主机也分担了查询和修改的压力,在需要其他节点出手时,会重定向:

    image-20230608113338056

Jedis

Jedis 是一个开源的 Java 编程语言 Redis 客户端,由 Xetorthio 开发和维护。它提供了简单易用、高性能的 API,可以连接到 Redis 服务器并执行各种操作,Jedis API 同 Redis 命令基本相同,特别简单易用。

单机版

  1. 创建 Maven 工程

  2. 导入 jedis 依赖

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.3.1</version>
    </dependency>
    
  3. 测试

    @Test
    public void testStandalone() {
        Jedis jedis = new Jedis("192.168.80.130", 6379);
        jedis.set("test", "penghao");
        String value = jedis.get("test");
        System.out.println(value);
    }
    

带连接池

@Test
public void testJedisPool() {
    // 配置连接池
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxTotal(20);
    jedisPoolConfig.setMaxIdle(5);
    jedisPoolConfig.setMinIdle(3);

    // 创建连接池
    JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.80.130", 6379);

    // 从连接池获取连接
    Jedis jedis = jedisPool.getResource();
    jedis.set("test2", "pool");
    String test2 = jedis.get("test2");
    System.out.println(test2);
}

使用集群

@Test
public void testCluster() {
    Set<HostAndPort> set = new HashSet<>();
    set.add(new HostAndPort("192.168.80.130", 7001));
    set.add(new HostAndPort("192.168.80.130", 7002));
    set.add(new HostAndPort("192.168.80.130", 7003));
    set.add(new HostAndPort("192.168.80.130", 7004));
    set.add(new HostAndPort("192.168.80.130", 7005));
    set.add(new HostAndPort("192.168.80.130", 7006));

    try (JedisCluster jedisCluster = new JedisCluster(set)) {
        jedisCluster.set("name", "cluster");
        String name = jedisCluster.get("name");
        System.out.println(name);
    }
}

使用 SpringBoot 整合 SpringDataRedis 操作 Redis

Spring Data 是 Spring 提供的对各种数据操作 API 的封装项目,它可以很方便地操作各种对象。其中一个二级项目 Spring Data Redis 对 Redis 操作进行了封装,把 Redis 不同值的类型放到一个 opsForXXX 方法中:

  • opsForValue:String 值
  • opsForList:List 列表
  • opsForHash:Hash 哈希表
  • opsForSet:Set 集合
  • opsForZSet:Sorted Set 有序集合

添加依赖

在 SpringBoot 项目中使用 Redis,需要加入其启动器:

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

配置 Redis 集群

spring:
  redis:
    cluster:
      nodes: 192.168.80.130:7001,192.168.80.130:7002,192.168.80.130:7003,192.168.80.130:7004,192.168.80.130:7005,192.168.80.130:7006

Redis 配置类

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));

        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

使用 Redis 做缓存的 Service 实现类

@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    private ProductMapper mapper;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public Product findProductById(Integer id) {

        String key = "product:" + id;
        // 先从 redis 中获取数据
        if (redisTemplate.hasKey(key)) {
            System.out.println("缓存命中,执行 Redis 查询");
            redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));
            return (Product) redisTemplate.opsForValue().get(key);
        }
        // 如果 redis 没有,再从 mybatis 获取
        System.out.println("执行 MySQL 查询");
        Product product = mapper.findProductById(id);
        // 设置缓存
        redisTemplate.opsForValue().set(key, product);
        return product;
    }
}