redis入门到精通

1 redis安装

1.1 安装

#没有wget的需要使用以下命令安装wget,方便对下载各种软件的安装文件
yum install wget
#进入redis的安装目录soft
cd /home/software
#下载redis安装文件
wget https://download.redis.io/releases/redis-5.0.5.tar.gz
#解压redis安装文件
tar xf redis-5.0.5.tar.gz
#进入redis目录
cd redis-5.0.5
ll
#编译
make
#出现make cc Command not found,make: *** [adlist.o] Error 127 问题时需要先安装gcc,然后清理编译环境,无问题直接跳至下一步
yum install gcc
make distclean
make
# 出现It's a good idea to run 'make test'再执行
make install

1.2 配置

redis有两个配置文件:分别是redis.conf(redis配置文件)和utils/redis_init_script(redis启动脚本)

1.2.1 redis.conf
## redis.conf
# 创建redis目录
mkdir /usr/local/redis -p
# 复制redis配置文件至指定目录
cp redis.conf /usr/local/redis/
# 进入redis目录
cd /usr/local/redis/
# 修改该配置文件
vim redis.conf

# 以下是redis.conf需要修改的内容
daemonize no ---> daemonize yes # 是否后台启动
dir ./ ---> dir /usr/local/redis/working #工作空间 如果不存在需要创建
bind 127.0.0.1 ---> bind 0.0.0.0 任意主机都能访问redis
#requirepass foobared ---> requirepass xxx #redis登陆密码 
1.2.2 redis_init_script
## utils/redis_init_script
# 复制启动脚本至/etc/init.d/
cp redis_init_script /etc/init.d/
vim redis_init_script
# 以下为redis脚本内容 其他无需改动
REDISPORT=6379 # 端口号
EXEC=/usr/local/bin/redis-server # redis客户端
CLIEXEC=/usr/local/bin/redis-cli # redis服务端

PIDFILE=/var/run/redis_${REDISPORT}.pid # redis进程文件
CONF="/usr/local/redis/redis.conf" # redis配置文件(修改为自己配置文件所在地)

$CLIEXEC -p $REDISPORT shutdown ---> $CLIEXEC -a xxx -p $REDISPORT shutdown # 赋予关闭全新

# 修改完成后 需要给脚本执行权限
chmod 777 redis_init_script
# 启动redis
./redis_init_script start
# 关闭
./redis_init_script stop
1.2.3 redis开机自启
## redis额外配置
# 实现开机自启 在redis_init_script加入以下两行
#chkconfig: 22345 10 90
#description: Start and Stop redis
# 在执行以下命令 注册服务
chkconfig redis_init_script on
# 开机自启后 如果想手动开启 需将redis配置文件放在制定路径
cp redis.conf /etc/redis/6379.conf

2 redis常用命令

2.1 redis 综合命令

1、DEL key

删除置顶的key

2、EXISTS key

检查给定的key是否存在

3、EXPIRE key seconds

为key设置过期时间

4、EXPIRE key timestamp

用时间戳的方式给key设置过期时间

5、PEXPIRE key milliseconds

设置key的过期时间以毫秒计

6、KEYS pattern

查找所有符合给定模式的key

7、MOVE key db

将当前数据库的key移动到数据库db当中

8、PERSIST key

移除key的过期时间,key将持久保存

9、PTTL key

以毫秒为单位返回key的剩余过期时间

10、TTL key

以秒为单位,返回给定key的剩余生存时间

11、RANDOMKEY

从当前数据库中随机返回一个key

12、RENAME key newkey

修改key的名称

13、RENAMENX key newkey

仅当newkey不存在时,将key改名为newkey

14、TYPE key

返回key所存储的值的类型

2.2 reids字符串命令

1、SET key value

2、GET key

3、GETRANGE key start end

返回key中字符串值的子字符

4、GETSET key value

将给定key的值设为value,并返回key的旧值

5、GETBIT KEY OFFSET

对key所储存的字符串值,获取指定偏移量上的位

6、MGET KEY1 KEY2

获取一个或者多个给定key的值

7、SETBIT KEY OFFSET VALUE

对key所是存储的字符串值,设置或清除指定偏移量上的位

**8、SETEX key seconds value**

将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。

9、SETNX key value

只有在 key 不存在时设置 key 的值。

10、SETRANGE key offset value

用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。

11、STRLEN key

返回 key 所储存的字符串值的长度。

12、MSET key value \[key value ...]

同时设置一个或多个 key-value 对。

13、MSETNX key value \[key value ...]

同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。

14、PSETEX key milliseconds value

这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。

15、INCR key

将 key 中储存的数字值增一。

16、INCRBY key increment

将 key 所储存的值加上给定的增量值(increment) 。

17、INCRBYFLOAT key increment

将 key 所储存的值加上给定的浮点增量值(increment) 。

18、DECR key

将 key 中储存的数字值减一。

19、DECRBY key decrement

key 所储存的值减去给定的减量值(decrement) 。

20、APPEND key value

如果 key 已经存在并且是一个字符串, APPEND 命令将 指定value 追加到改 key 原来的值(value)的末尾。

2.3 Redis hash 命令

1、HDEL key field1 \[field2]

删除一个或多个哈希表字段

2、HEXISTS key field

查看哈希表 key 中,指定的字段是否存在。

3、HGET key field

获取存储在哈希表中指定字段的值。

4、HGETALL key

获取在哈希表中指定 key 的所有字段和值

5、HINCRBY key field increment

为哈希表 key 中的指定字段的整数值加上增量 increment 。

6、HINCRBYFLOAT key field increment

为哈希表 key 中的指定字段的浮点数值加上增量 increment 。

7、HKEYS key

获取所有哈希表中的字段

8、HLEN key

获取哈希表中字段的数量

9、HMGET key field1 \[field2]

获取所有给定字段的值

10、HMSET key field1 value1 \[field2 value2 ]

同时将多个 field-value (域-值)对设置到哈希表 key 中。

11、HSET key field value

将哈希表 key 中的字段 field 的值设为 value 。

12、HSETNX key field value

只有在字段 field 不存在时,设置哈希表字段的值。

13、HVALS key

获取哈希表中所有值

14、HSCAN key cursor \[MATCH pattern] \[COUNT count]

迭代哈希表中的键值对。

2.4 Redis 列表命令(list)

1、BLPOP key1 \[key2 ] timeout

移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

2、BRPOP key1 \[key2 ] timeout

移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

3、BRPOPLPUSH source destination timeout

从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

4、LINDEX key index

通过索引获取列表中的元素

5、LINSERT key BEFORE|AFTER pivot value

在列表的元素前或者后插入元素

6、LLEN key

获取列表长度

7、LPOP key

移出并获取列表的第一个元素

8、LPUSH key value1 \[value2]

将一个或多个值插入到列表头部

9、LPUSHX key value

将一个值插入到已存在的列表头部

10、LRANGE key start stop

获取列表指定范围内的元素

11、LREM key count value

移除列表元素

12、LSET key index value

通过索引设置列表元素的值

13、LTRIM key start stop

对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。

14、RPOP key

移除并获取列表最后一个元素

15、RPOPLPUSH source destination

移除列表的最后一个元素,并将该元素添加到另一个列表并返回

16、RPUSH key value1 \[value2]

在列表中添加一个或多个值

17、RPUSHX key value

为已存在的列表添加值

2.5 Redis 集合命令(set)

1、SADD key member1 \[member2]

向集合添加一个或多个成员

2、SCARD key

获取集合的成员数

3、SDIFF key1 \[key2]

返回给定所有集合的差集

4、SDIFFSTORE destination key1 \[key2]

返回给定所有集合的差集并存储在 destination 中

5 、SINTER key1 \[key2]

返回给定所有集合的交集

6、SINTERSTORE destination key1 \[key2]

返回给定所有集合的交集并存储在 destination 中

7、SISMEMBER key member

判断 member 元素是否是集合 key 的成员

8、SMEMBERS key

返回集合中的所有成员

9、SMOVE source destination member

将 member 元素从 source 集合移动到 destination 集合

10、SPOP key

移除并返回集合中的一个随机元素

11、SRANDMEMBER key \[count]

返回集合中一个或多个随机数

12、SREM key member1 \[member2]

移除集合中一个或多个成员

13、SUNION key1 \[key2]

返回所有给定集合的并集

14、SUNIONSTORE destination key1 \[key2]

所有给定集合的并集存储在 destination 集合中

15、SSCAN key cursor \[MATCH pattern] \[COUNT count]

迭代集合中的元素

2.6 Redis 有序集合命令

1、ZADD key score1 member1 \[score2 member2]

向有序集合添加一个或多个成员,或者更新已存在成员的分数

2、ZCARD key

获取有序集合的成员数

3、ZCOUNT key min max

计算在有序集合中指定区间分数的成员数

4、ZINCRBY key increment member

有序集合中对指定成员的分数加上增量 increment

5、ZINTERSTORE destination numkeys key \[key ...]

计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中

6、ZLEXCOUNT key min max

在有序集合中计算指定字典区间内成员数量

7、ZRANGE key start stop \[WITHSCORES]

通过索引区间返回有序集合成指定区间内的成员

8、ZRANGEBYLEX key min max \[LIMIT offset count]

通过字典区间返回有序集合的成员

9、ZRANGEBYSCORE key min max \[WITHSCORES] \[LIMIT]

通过分数返回有序集合指定区间内的成员

10、ZRANK key member

返回有序集合中指定成员的索引

11、ZREM key member \[member ...]

移除有序集合中的一个或多个成员

12、ZREMRANGEBYLEX key min max

移除有序集合中给定的字典区间的所有成员

13、ZREMRANGEBYRANK key start stop

移除有序集合中给定的排名区间的所有成员

14、ZREMRANGEBYSCORE key min max

移除有序集合中给定的分数区间的所有成员

15、ZREVRANGE key start stop \[WITHSCORES]

返回有序集中指定区间内的成员,通过索引,分数从高到底

16、ZREVRANGEBYSCORE key max min \[WITHSCORES]

返回有序集中指定分数区间内的成员,分数从高到低排序

17、ZREVRANK key member**

返回有序集合中指定成员的排名,有序集成员按分数值递减从大到小排序

18、ZSCORE key member

返回有序集中,成员的分数值

19、ZUNIONSTORE destination numkeys key \[key ...]

计算给定的一个或多个有序集的并集,并存储在新的 key 中

20、ZSCAN key cursor \[MATCH pattern] \[COUNT count]

迭代有序集合中的元素(包括元素成员和元素分值)

2.7 Redis 发布订阅命令

1、PSUBSCRIBE pattern \[pattern ...]

订阅一个或多个符合给定模式的频道。

2、PUBSUB subcommand \[argument \[argument ...]

查看订阅与发布系统状态。

3、PUBLISH channel message

将信息发送到指定的频道。

4、PUNSUBSCRIBE \[pattern \[pattern ...]]

退订所有给定模式的频道。

5、SUBSCRIBE channel \[channel ...]

订阅给定的一个或多个频道的信息。

6、UNSUBSCRIBE \[channel \[channel ...]]

指退订给定的频道。

示例:

    redis 127.0.0.1:6379> SUBSCRIBE redisChat
    Reading messages... (press Ctrl-C to quit)
    "subscribe"
    "redisChat"
    (integer) 1

现在,我们先重新开启个 redis 客户端,然后在同一个频道 redisChat 发布两次消息,订阅者就能接收到消息。 
redis 127.0.0.1:6379> PUBLISH redisChat 
"Redis is a great caching technique"
(integer) 1
    订阅者的客户端会显示如下消息
    1) "message"
    2) "redisChat"
    3) "Redis is a great caching technique"

2 redis模型

2.1 阻塞与非阻塞

阻塞和非阻塞是操作系统中两种不同的进程调度方式,它们的主要区别在于是否等待操作完成。

阻塞(Blocking)

  • 在I/O操作或系统调用时,如果进程需要等待某些条件满足(如数据读取完成),那么该进程会被挂起,直到条件满足为止。在此期间,进程不会消耗CPU资源,但也不会释放占用的锁或资源。

  • 阻塞通常发生在I/O操作的准备阶段和操作阶段。在准备阶段,进程等待数据准备好;在操作阶段,进程等待数据传输完成。

非阻塞(Non-blocking)

  • 进程在发起一个操作后,不需要等待操作完成就可以继续执行其他任务。这样,CPU可以去做其他有用的工作,而不是等待I/O操作。

  • 非阻塞IO操作通常会立即返回,告知调用者操作是否成功,或者是否还需要继续等待。

总的来说,阻塞和非阻塞的关键区别在于,阻塞操作会挂起进程直到操作完成,而非阻塞操作则允许进程在不等待结果的情况下继续执行。

2.2 redis模型

对于 redis 来说,它采用的是 I/O 多路复用技术而不是真正的多线程模型。其基本流程:

1、redis-server主线程作为生产者:

∙ 当有新的客户端连接请求到达时,主线程会将对应的客户端套接字加入到clients_pending_read 队列中。这表示该连接上有数据可读,需要被处理。

∙当有客户端数据写入请求到达时,主线程会将对应的客户端套接字加入到 clients_pending_write 队列中。这表示该连接上可以进行写操作。

2、redis-server主线程作为消费者: 

∙ 主线程通过循环遍历 clients_pending_read 队列中的客户端套接字,并将其分配给合适的 I/O 线程处理。主线程会根据负载均衡策略(如轮询或哈希)来决定将客户端套接字分发给哪个 I/O 线程的专属队列。 

∙ 类似地,主线程也会从 clients_pending_write 队列中获取客户端套接字,并将其分配给适当的 I/O 线程处理。

3、I/O 线程执行任务:

∙每个 I/O 线程拥有一个专属队列(如 io_threads_list[id]),主线程将客户端套接字分配给指定的 I/O 线程,并将其加入到对应的队列中。

∙ I/O 线程通过从自己的队列中获取客户端套接字,进行实际的读写操作和请求处理。一旦完成操作,也可以将结果返回给主线程。

通过这种队列模型和任务调度方式,主线程在兼顾生产者和消费者角色的同时,能够高效地将任务分发给对应的 I/O 线程进行处理,以提高并发性能和系统的吞吐量。同时,这种设计还能避免多线程并发带来的同步问题和竞争条件,保证了系统的稳定性和可靠性。

3 springboot整合redis

1 引入依赖

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

2 添加redis配置

spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=foobard

3 redis工具类

package com.hogan.util;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * RedisUtil
 *
 * @author hogan
 * @since 2024/2/5 9:18 AM
 */
@Component
public class RedisUtil {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // =============================common============================

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return stringRedisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                stringRedisTemplate.delete(key[0]);
            } else {
                stringRedisTemplate.delete(String.valueOf(CollectionUtils.arrayToList(key)));
            }
        }
    }

    // ============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public String get(String key) {
        return key == null ? null : stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, String value) {
        try {
            stringRedisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, String value, long time) {
        try {
            if (time > 0) {
                stringRedisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return stringRedisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return stringRedisTemplate.opsForValue().increment(key, -delta);
    }

    // ================================Map=================================

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return stringRedisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return stringRedisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, String> map) {
        try {
            stringRedisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, String> map, long time) {
        try {
            stringRedisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            stringRedisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            stringRedisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        stringRedisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return stringRedisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return stringRedisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return stringRedisTemplate.opsForHash().increment(key, item, -by);
    }

    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     */
    public Set<String> sGet(String key) {
        try {
            return stringRedisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return stringRedisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, String... values) {
        try {
            return stringRedisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, String... values) {
        try {
            Long count = stringRedisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return stringRedisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = stringRedisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<String> lGet(String key, long start, long end) {
        try {
            return stringRedisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return stringRedisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return stringRedisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, String value) {
        try {
            stringRedisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, String value, long time) {
        try {
            stringRedisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, List<String> value) {
        try {
            stringRedisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, List<String> value, long time) {
        try {
            stringRedisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, String value) {
        try {
            stringRedisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            return stringRedisTemplate.opsForList().remove(key, count, value);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}



4 redis持久化机制

4.1 rdb持久化

RDB持久化机制是Redis提供的一种数据持久化到磁盘的策略,它通过创建内存快照来保存Redis数据

RDB持久化的优点主要包括:

  • 性能最大化:在进行RDB持久化时,Redis会通过fork操作创建一个子进程来完成实际的持久化工作,这样主进程可以继续处理命令而不受持久化操作的影响。

  • 恢复速度快:由于RDB文件通常比AOF文件小且加载速度更快,因此在系统崩溃后,使用RDB恢复整个数据集的速度要比AOF快许多。

  • 方便灾难恢复:RDB文件是一个紧凑的全量备份,适合用于灾难恢复,并且可以将这个文件保存在安全的地方,以便于发生故障时进行恢复。

RDB持久化的缺点也是显而易见的,具体为:

  • 数据安全性较低:RDB采用的是间隔性的备份方式,如果在两次备份之间发生了故障,那么这期间的数据将会丢失。

  • 数据一致性:与AOF相比,RDB在某些情况下可能无法保证完全一致性。例如,如果在一个大型数据库中执行了一个耗时的操作,恰好在操作完成后未及时进行RDB快照,紧接着发生了故障,那么这个耗时的操作结果就会丢失。

# save <seconds> <changes> 周期性执行RDB持久化条件的设置格式
# 默认配置 每一个变化 900秒后会进行持久化(# save <seconds> <changes> 周期性执行RDB持久化条件的设置格式
save 900 1
save 300 10
save 60 10000

# 文件名称
dbfilename dump.rdb

# 文件保存路径
dir /home/work/app/redis/data/

# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes

# 是否压缩
rdbcompression yes

# 导入时是否检查,这里是使用CRC64算法来进行数据校验,会增加性能消耗
rdbchecksum yes

)
save 900 1
save 300 10
save 60 10000

# 文件名称
dbfilename dump.rdb

# 文件保存路径
dir /home/work/app/redis/data/

# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes

# 是否压缩
rdbcompression yes

# 导入时是否检查,这里是使用CRC64算法来进行数据校验,会增加性能消耗
rdbchecksum yes

4.2 aop持久化

4.2.1 aop配置

AOF(Append Only File)持久化机制是Redis提供的一种将写操作追加到文件末尾的策略,它通过记录所有的写命令来保证数据的安全性和一致性

AOF持久化的优点包括:

  • 更高的数据安全性:由于AOF记录了所有的写操作,因此在出现故障时,可以通过重新执行这些命令来恢复数据,从而保证了数据的完整性。

  • 更灵活的持久化策略:AOF持久化的频率是可以配置的,用户可以根据需要设置每秒写入或每个命令写入等不同的策略。

然而,AOF持久化也存在一些缺点:

  • 文件体积较大:因为AOF文件是文本格式,记录了所有的写命令,所以它的体积通常比RDB文件要大,这可能导致更多的磁盘空间占用。

  • 恢复速度相对较慢:在系统崩溃后,AOF文件需要重新执行其中记录的所有写命令来恢复数据,这个过程可能比加载RDB文件更耗时。

# 开启redis-aof持久化机制
appendonly yes
# 每秒存储一次修改内容
appendfsync everysec
# AOF文件体积增长了指定的百分比并且文件体积大于指定大小时,Redis会在后台重写AOF文件。重写之后文件会更小,命令更精简
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
4.2.2 redis误操作flushdb操作怎么办

恢复思路 如果Redis启用AOF持久化,则可以根据AOF文件进行恢复。AOF文件是将所有写指令以文本形式保存的,可以将AOF文件中的flushdb/flushall命令删除,再重启Redis即可。

误操作后首先要做的事 如果发生了AOF重写,AOF文件中的数据将被覆盖,数据将无法恢复。所以在误操作之后,首先要做的是避免发生AOF重写。第一,不要手动执行bgrewriteaof命令;第二,修改触发AOF重写的相关参数,将参数值调大,避免自动触发AOF重写。

恢复过程
1)调整触发AOF重写的参数值:
CONFIG SET auto-aof-rewrite-percentage 1000
CONFIG SET auto-aof-rewrite-min-size 100000000000
2)删除AOF文件中误操作的写指令:
\*1
\$8
FLUSHALL
3)检查AOF文件:
redis-check-aof appendonly\_6379.aof
4)重启Redis服务,恢复数据;

4.3 aop和rdb的联系

AOF和RDB是Redis提供的两种不同的持久化机制,它们各自有不同的特点和使用场景

RDB持久化是通过创建内存快照来保存Redis数据的一种方式。它的主要优点是可以生成二进制压缩文件,这使得恢复速度非常快。但缺点是在两次快照之间如果发生了故障,那么这期间的数据可能会丢失。

AOF持久化则是通过记录所有的写操作命令来保持数据的一致性和完整性。它的优点在于不易丢失数据,因为每一步操作都被记录下来,但这也导致了相对较大的性能影响,并且在数据恢复时可能比RDB慢,因为需要重新执行所有记录的命令。

在实际应用中,可以根据业务需求选择合适的持久化策略。如果对数据的实时性和完整性有较高要求,那么AOF可能是更好的选择。如果更关心恢复速度和磁盘空间的利用,RDB可能更适合。此外,还可以结合使用RDB和AOF,这样可以兼顾数据的安全性和恢复效率。就是让 RDB 快照以一定频率执行,在两次快照之间使用 AOF 日志记录这期间的所有命令操作。

  • 如果服务器开启了 AOF 持久化功能,优先使用 AOF 文件,因为AOF 更新频率通常要比 RDB 文件要高

  • 只有当 AOF 持久化功能关闭时,服务器才会使用 RDB 文件来还原数据状态

总结来说,RDB和AOF各有优势和不足,选择合适的持久化机制取决于具体的应用场景和对数据安全性、恢复速度的需求。在实际使用中,可以根据需要选择单独使用某一种或者两者结合的方式来实现最佳的数据保护效果。

5 redis主从架构

5.1 主从原理

Redis主从模式基于读写分离的原则,通过一个主节点和多个从节点构成高可用的服务架构。以下是其原理和工作模式的详细解释:

角色定义

  • 主节点(Master):负责处理所有写操作,以及客户端的读操作。

  • 从节点(Slave):复制主节点的数据,可以处理客户端的读操作,从而分担主节点的负载。数据一致性

  • 从服务器连接到主服务器,并发送SYNC命令。

  • 主服务器接收到SYNC命令后,开始执行BGSAVE命令生成RDB文件。

  • 主服务器BGSAVE执行完毕后,将RDB文件发送给从服务器。

  • 从服务器收到RDB文件后,载入并应用这个RDB文件。

  • 主服务器在生成RDB期间,将新的写操作保存在内存中。

  • 从服务器载入RDB后,主服务器将这些新的写操作发送给从服务器。

    故障恢复

  • 在主从模式中,如果主节点出现故障,需要手动将从节点提升为主节点来继续服务。

  • 这种模式的故障恢复效率不高,因为需要管理员介入进行手动操作。

5.2 配置

#在从服务器的配置文件中添加如下指令:
slaveof <master-ip> <master-port>
#或者,在启动Redis服务时使用命令行参数:
redis-server --slaveof <master-ip> <master-port>
#示例代码:
# 配置主服务器地址和端口
slaveof 192.168.1.100 6379
#在实际应用中,可以通过INFO replication命令来查看主从复制的状态和信息。
127.0.0.1:6379> info replication   //获取服务器的详细信息
# Replication
role:master    //当前角色 : 主服务器
connected_slaves:0  //连接的从服务器的个数
master_repl_offset:0  //是复制流中的一个偏移量,master处理完写入命令后,会把命令的字节长度做累加记录,统计在该字段。该字段也是实现部分复制的关键字段。
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

5.3 redis主从无磁盘化

1.直接从一个内存直接写到另外一个内存

2.基于网络(socket)的传输,

3.如果服务器使用的硬件磁盘配置十分差的话,对应额的读写功能也是很差的,那么主从复制的效率也会很低下,无磁盘化就是不经过磁盘,直接通过内存进行操作提高效率

4.如果你的磁盘工作效率十分低下,但是你的网络速度十分快的话,可以使用这个方式(该方式默认是关闭的)

配置如下:

#注: 该功能尽在试验阶段 不推荐在生产使用
repl-diskless-sync yes 

6 redis刷新过期策略

6.1 redis过期策略

Redis的键过期策略主要有两种:惰性删除和定时删除。

  1. 惰性删除:当一个键被访问时,Redis会检查它是否过期,如果过期就删除它。

  2. 定时删除:Redis默认每100ms会随机抽查一些设置了过期时间的键,检查并删除其中已经过期的键。

注意,Redis不会在每个命令执行前对键进行检查,以避免性能损失。因此,过期键只会在访问时检查。

此外,Redis还有惰性过期和定时过期混合使用。在键空间事件触发时,Redis会进行过期键的检查,并根据情况进行删除。

Redis的这种过期策略是为了平衡CPU和内存资源,因为过于频繁的删除操作会占用CPU资源,而频繁访问过期键则会增加内存的使用。

6.2 redis内存满刷新策略

Redis主要提供了多种内存淘汰策略来处理数据过期和内存回收。这些策略决定了在内存使用量超过maxmemory限制时,Redis如何选择要删除的数据。以下是具体的过期策略及其配置方法:

  1. noeviction:这是默认的淘汰策略。当达到内存使用限制时,如果客户端尝试执行可能会使用更多内存的命令,Redis会返回错误。这种策略保证了不会主动删除任何数据,但可能会导致写入操作失败。

  2. volatile-lru:这种策略会删除设置了过期时间且最近最少使用的键(Least Recently Used, LRU)。这意味着Redis会优先淘汰那些不常访问且有过期时间的键。

  3. allkeys-lru:与volatile-lru类似,但这种策略会从所有键中选择最近最少使用的数据进行淘汰,无论它们是否设置了过期时间。

  4. volatile-random:这种策略随机删除设置了过期时间的键,不考虑访问频率。

  5. allkeys-random:类似于volatile-random,但会从所有键中随机选择数据进行淘汰。

  6. volatile-ttl:这种策略会根据键的剩余存活时间来决定淘汰哪个键,越早过期的键越先被淘汰。

  7. noeviction:不进行数据淘汰,仅在客户端尝试执行写入操作时返回错误。

要配置Redis的过期策略,可以通过修改配置文件或者使用CONFIG SET命令来实现。例如,要将策略设置为volatile-lru,可以在配置文件中添加或修改以下行:

maxmemory-policy volatile-lru

7 redis哨兵模式

7.2 哨兵模式简介

主从复制模式,它是属于 Redis 多机运行的基础,但这种模式本身存在一个致命的问题,当主节点奔溃之后,需要人工干预才能恢复 Redis 的正常使用。

我们需要一个自动的工具——Redis Sentinel(哨兵模式)来把手动的过程变成自动的,让 Redis 拥有自动容灾恢复(failover)的能力。

哨兵就相当于对主从服务器做一个监视的任务。一旦发现主服务器宕机了,就迅速启动相应的规则将某一台从服务器升级为主服务器,无需人工干预,更稳定更快。

7.2 配置

###普通配置
port 27001
# 保护模式关闭,这样其他服务起就可以访问此台redis
protected-mode no
# 哨兵模式是否后台启动,默认no,改为yes
daemonize yes
pidfile /var/run/redis-sentinel.pid
# log日志保存位置
logfile /usr/local/redis/sentinel/redis-sentinel.log
# 工作目录
dir /usr/local/redis/sentinel

###核心配置
# 核心配置。
#sentinel monitor <master-name> <ip> <redis-port> <quorum>
# 第三个参数:哨兵名字,可自行修改。(若修改了,那后面涉及到的都得同步) 
# 第四个参数:master主机ip地址
# 第五个参数:redis端口号
# 第六个参数:哨兵的数量。比如2表示,当至少有2个哨兵发现master的redis挂了,
#               那么就将此master标记为宕机节点。
#               这个时候就会进行故障的转移,将其中的一个从节点变为master
sentinel monitor mymaster 192.168.101.123 7001 2
# master中redis的密码
sentinel auth-pass mymaster 123456
# 哨兵从master节点宕机后,等待多少时间(毫秒),认定master不可用。
# 默认30s,这里为了测试,改成10s
sentinel down-after-milliseconds mymaster 10000
# 当替换主节点后,剩余从节点重新和新master做同步的并行数量,默认为 1
sentinel parallel-syncs mymaster 1
# 主备切换的时间,若在3分钟内没有切换成功,换另一个从节点切换
sentinel failover-timeout mymaster 180000

7.3 启动

# 一定要先启动redis主从 再启动哨兵
redis-sentinel /etc/redis-sentinel.conf 

7.4 主从切换原理

7.4.1 哨兵如何判断master宕机
7.4.1.1 主观下线

  这个就是上面介绍的第一个定时任务做的事情,当sentinel节点向master发送一个PING命令,如果超过own-after-milliseconds(默认是30s,这个在sentinel的配置文件中可以自己配置)时间都没有收到有效回复,不好意思,我就认为你挂了,就是说为的主观下线(SDOWN),修改其flags状态为SRI_S_DOWN

7.4.1.2 客观下线

  要了解什么是客观下线要先了解几个重要参数:

  • quorum:如果要认为master客观下线,最少需要主观下线的sentinel节点个数,举例:如果5个sentinel节点,quorum = 2,那只要2个sentinel主观下线,就可以判断master客观下线

  • majority:如果确定了master客观下线了,就要把其中一个slaver切换成master,做这个事情的并不是整个sentinel集群,而是sentinel集群会选出来一个sentinel节点来做,那怎么选出来的呢,下面会讲,但是有一个原则就是需要大多数节点都同意这个sentinel来做故障转移才可以,这个大多数节点就是这个参数。注意:如果sentinel节点个数5,quorum=2,majority=3,那就是3个节点同意就可以,如果quorum=5,majority=3,这时候majority=3就不管用了,需要5个节点都同意才可以。

  • configuration epoch:这个其实就是version,类似于中国每个皇帝都要有一个年号一样,每个新的master都要生成一个自己的configuration epoch,就是一个编号

7.4.1.3 客观下线处理过程
  1. 每个主观下线的sentinel节点都会向其他sentinel节点发送 SENTINEL is-master-down-by-addr ip port current_epoch runid,(ip:主观下线的服务id,port:主观下线的服务端口,current_epoch:sentinel的纪元,runid:*表示检测服务下线状态,如果是sentinel 运行id,表示用来选举领头sentinel(下面会讲选举领头sentinel))来询问其它sentinel是否同意服务下线。

  2. 每个sentinel收到命令之后,会根据发送过来的ip和端口检查自己判断的结果,如果自己也认为下线了,就会回复,回复包含三个参数:down_state(1表示已下线,0表示未下线),leader_runid(领头sentinal id),leader_epoch(领头sentinel纪元)。由于上面发送的runid参数是*,这里后两个参数先忽略。

  3. sentinel收到回复之后,根据quorum的值,判断达到这个值,如果大于或等于,就认为这个master客观下线

7.4.2 选择领头sentinel的过程

  到现在为止,已经知道了master客观下线,那就需要一个sentinel来负责故障转移,那到底是哪个sentinel节点来做这件事呢?需要通过选举实现,具体的选举过程如下:

  1. 判断客观下线的sentinel节点向其他节点发送SENTINEL is-master-down-by-addr ip port current_epoch runid(注意:这时的runid是自己的run id,每个sentinel节点都有一个自己运行时id)

  2. 目标sentinel回复,由于这个选择领头sentinel的过程符合先到先得的原则,举例:sentinel1判断了客观下线,向sentinel2发送了第一步中的命令,sentinel2回复了sentinel1,说选你为领头,这时候sentinel3也向sentinel2发送第一步的命令,sentinel2会直接拒绝回复

  3. 当sentinel发现选自己的节点个数超过majority(注意上面写的一种特殊情况quorum>majority)的个数的时候,自己就是领头节点

  4. 如果没有一个sentinel达到了majority的数量,等一段时间,重新选举

7.4.3 故障转移过程

  通过上面的介绍,已经有了领头sentinel,下面就是要做故障转移了,故障转移的一个主要问题和选择领头sentinel问题差不多,到底要选择哪一个slaver节点来作为master呢?按照我们一般的常识,我们会认为哪个slaver中的数据和master中的数据相识度高哪个slaver就是master了,其实哨兵模式也差不多是这样判断的,不过还有别的判断条件,详细介绍如下:

  在进行选择之前需要先剔除掉一些不满足条件的slaver,这些slaver不会作为变成master的备选

  • 剔除列表中已经下线的从服务

  • 剔除有5s没有回复sentinel的info命令的slaver

  • 剔除与已经下线的主服务连接断开时间超过 down-after-milliseconds*10+master宕机时长的slaver

7.4.3.1 选主过程
  1. 选择优先级最高的节点,通过sentinel配置文件中的replica-priority配置项,这个参数越小,表示优先级越高

  2. 如果第一步中的优先级相同,选择offset最大的,offset表示主节点向从节点同步数据的偏移量,越大表示同步的数据越多

  3. 如果第二步offset也相同,选择run id较小的

7.4.3.2 后续事项

  新的主节点已经选择出来了,并不是到这里就完事了,后续还需要做一些事情,如下

  1. 领头sentinel向别的slaver发送slaveof命令,告诉他们新的master是谁谁谁,你们向这个master复制数据

  2. 如果之前的master重新上线时,领头sentinel同样会给起发送slaveof命令,将其变成从节点

7.5 优点和缺点

Redis哨兵模式的优点主要包括自动化监控和高可用性,而缺点则涉及到内存使用效率和分布式存储的限制。具体来说:

优点方面

  • 自动化监控:哨兵模式能够自动监测节点的健康状况,无需人工干预即可实现节点切换,这大大减少了系统管理员的管理负担。

  • 高可用性:在主节点出现问题时,哨兵模式可以快速切换到备份节点,从而保持服务的连续性和数据的完整性。

缺点方面

  • 内存使用效率:在哨兵模式下,每个Redis服务器都存储相同的数据副本,这可能导致内存资源的浪费。特别是在数据量较大的情况下,这种模式可能不是最经济的存储方式。

  • 分布式存储限制:虽然哨兵模式实现了一定程度的高可用性和故障转移,但它并不支持像Redis Cluster那样的分布式存储。在Redis 3.0之后引入的Cluster模式中,每个节点可以存储不同的数据,从而实现更高效的内存使用和更好的水平扩展能力。

总的来说,哨兵模式适合需要高可用性和自动化故障转移的场景,但在需要考虑内存使用效率和分布式存储需求的情况下,可能需要考虑使用其他模式,如Redis Cluster。

7.6 springboot整合哨兵模式

7.6.1 导入依赖
<dependencies>
    <!-- Redis依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>
7.6.2 redis哨兵配置
# 多哨兵,则nodes用数组格式,也就是横线
spring:
  redis:
    sentinel:
      master: your-redis-master  # 指定Redis的主节点名称
      nodes:  # 指定一个或多个哨兵节点的地址和端口号
        - host: your-sentinel1-host
          port: your-sentinel1-port
        - host: your-sentinel2-host
          port: your-sentinel2-port
        - host: your-sentinel3-host
          port: your-sentinel3-port
    password: your-redis-password
    # 在哨兵模式中,Redis哨兵节点的配置通常与主节点保持一致,因为哨兵节点不存储数据,它们只负责监控节点的可用性。
    # 所以你不需要在哨兵节点的配置中指定用户名和密码,只需要在主节点的配置中指定密码即可

注入redisTemplate即可使用

8 redis集群

8.1 redis集群简介

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存。所以在 redis3.0 上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的内容。Redis 集群是由多个主从节点群组成的分布式服务集群,具有复制、高可用和分片特性。这种集群模式没有中心节点,可水平扩展,主要是针对海量数据、高并发、高可用的场景。

Cluster 集群模式主要有以下三个特性:

1. 分片存储:Redis3.0 加入了 Redis 的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的 master 节点上面,从而解决了海量数据的存储问题。

2. 指令转换:Redis 集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一 Redis 实例一样,不需要任何代理中间件,当客户端操作的 key 没有分配到该 node 上时,Redis 会返回转向指令,指向正确的 Redis 节点。

3. 主从和哨兵:Redis 也内置了高可用机制,支持 N 个 master 节点,每个 master 节点都可以挂载多个 slave 节点,当 master 节点挂掉时,集群会提升它的某个 slave 节点作为新的 master 节点。

Redis 集群可以看成多个主从架构组合起来的,每一个主从架构可以看成一个节点。

8.2 redis集群配置

include /opt/redis-stable/cluster/redis.conf
# 当我们采用yes时,redis会在后台运行,此时redis将一直运行,除非手动kill该进程。
daemonize yes
bind 0.0.0.0
dir /opt/redis-stable/cluster/
port 6379
dbfilename dump_6379.rdb
pidfile /var/run/redis_6379.pid
logfile "/opt/redis-stable/cluster/log/6379.log"
# 开启集群设置
cluster-enabled yes
# 设置节点配置文件
cluster-config-file node-6379.conf
# 设置节点失联时间,超过该时间(毫秒),集群自动进行主从切换
cluster-node-timeout 15000

8.3 启动

# 分别启动每个单独的redis
/usr/local/redis/redis-server /opt/redis-stable/cluster/redis-6379.conf
# 创建redis集群
/usr/local/redis/redis-cli -a password --cluster create --cluster-replicas 1 192.168.0.101:6379 192.168.0.101:6380 192.168.0.101:6381 192.168.0.101:6389 192.168.0.101:6390 192.168.0.101:6391
# 登录redis 加-c參数,节点之间就可以互相跳转 在set key时 redis会自动计算槽位 自懂跳转至对应的机器赋值 get key时 redis也会自动计算槽位 自动跳转取值
redis-cli -p 6001 -c

8.4 槽位定位算法

Cluster 默认会对 key 值使用 crc32 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。

Cluster 还允许用户强制某个 key 挂在特定槽位上,通过在 key 字符串里面嵌入 tag 标记,这就可以强制 key 所挂在的槽位等于 tag 所在的槽位。

当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。关于这一点在后面会重点进行演示! Redis集群不能保证强一致性。一些已经向客户端确认写成功的操作,会在某些不确定的情况下丢失。

产生写操作丢失的第一个原因,是因为主从节点之间使用了异步的方式来同步数据。

一个写操作是这样一个流程:

客户端向主节点B发起写的操作 主节点B回应客户端写操作成功 主节点B向它的从节点B1,B2,B3同步该写操作 从上面的流程可以看出来,主节点B并没有等从节点B1,B2,B3写完之后再回复客户端这次操作的结果。所以,如果主节点B在通知客户端写操作成功之后,但同步给从节点之前,主节点B故障了,其中一个没有收到该写操作的从节点会晋升成主节点,该写操作就这样永远丢失了。

如果真的需要,Redis集群支持同步复制的方式,通过WAIT 指令来实现,这可以让丢失写操作的可能性降到很低。但就算使用了同步复制的方式,Redis集群依然不是强一致性的,在某些复杂的情况下,比如从节点在与主节点失去连接之后被选为主节点,不一致性还是会发生。

8.5 springboot整合redis集群

8.5.1 导入依赖
<dependencies>
    <!-- Redis依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>
8.5.2 加入redis配置
# 集群节点(host:port),多个之间用逗号隔开
spring.redis.cluster.nodes=192.168.115.239:6379,192.168.115.239:6380,192.168.115.239:6381,192.168.115.239:6389,192.168.115.239:6390,192.168.115.239:6391
# 连接超时时间(毫秒)
spring.redis.timeout=60000
spring.redis.password=123456

9 redis缓存穿透

9.1 简介

缓存穿透现象是指查询一个不存在的数据,这种情况无论是缓存层还是存储层都无法命中,导致每次请求都需要访问数据库

处理缓存穿透问题的方法有以下几种:

  1. 空对象或缺省值缓存:在数据库查询不到数据时,可以将一个空对象或者业务上约定的缺省值缓存起来,这样后续同样的查询可以直接从缓存中获取,减少对数据库的压力。

  2. 预校验:在控制层先对查询参数进行校验,不合法的直接丢弃,避免不必要的数据库查询。

  3. 布隆过滤器:使用布隆过滤器(Bloom Filter)来拦截可能发生缓存穿透的查询。布隆过滤器可以高效地判断一个key是否可能存在于数据库中,不存在则直接返回,避免无意义的数据库查询。

  4. 网关层面限制:设置每个IP的访问阈值,防止恶意攻击导致的缓存穿透。

总的来说,缓存穿透会对数据库造成较大压力,特别是在高并发情况下,因此采取适当的预防措施是必要的。

9.2 布隆过滤器

布隆过滤器是一种基于概率的数据结构,用于判断一个元素是否属于某个集合

原理: 布隆过滤器由一个二进制向量(位数组)和几个哈希函数组成。当一个元素被加入集合时,它会被几个不同的哈希函数处理,得到几个位数组的索引,并将这些位置置为1。当查询一个元素时,同样使用这些哈希函数进行计算,如果所有计算出的位都是1,那么元素可能属于集合;如果有任何一个位是0,那么元素肯定不在集合中。由于哈希函数的随机性和位数组的大小有限,存在一定的误判率,即可能会将不属于集合的元素误判为属于集合。

优点

  • 空间效率:布隆过滤器只需要很小的空间就能表示一个大集合。

  • 查询时间:无论集合大小,查询时间都是常数级别。

  • 插入和删除操作:虽然不支持删除操作,但插入操作非常快速。

缺点

  • 误判率:布隆过滤器存在误判的可能,即可能会将不存在的元素判定为存在。

  • 不支持删除操作:由于删除一个元素会影响到其他元素的哈希位,所以布隆过滤器不支持删除操作。

总的来说,布隆过滤器是一种空间和时间效率都很高的数据结构,但由于其基于概率的特性,使用时需要根据具体场景权衡其优缺点。

9.3 springboot整合布隆过滤器

在Spring Boot中使用布隆过滤器,可以通过以下步骤进行:

引入依赖:在项目的pom.xml文件中添加Guava库的依赖,因为Guava提供了布隆过滤器的实现。

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version>
</dependency>

    

创建布隆过滤器:在需要使用布隆过滤器的地方,创建一个Guava的布隆过滤器实例。

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterExample {
    private static final int EXPECTED_INSERTIONS = 1000;
    private static final double FPP = 0.01; // False Positive Probability

    private BloomFilter<String> bloomFilter;

    public BloomFilterExample() {
		//字符集 |  数组大小  |  误判率
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(), EXPECTED_INSERTIONS, FPP);
    }
}

添加元素:使用put()方法将元素添加到布隆过滤器中。

bloomFilter.put("example");    

查询元素:使用mightContain()方法查询一个元素是否可能在布隆过滤器中。

boolean mightContain = bloomFilter.mightContain("example");

10 redis缓存雪崩

Redis缓存雪崩是指当缓存服务器由于某些原因无法正常提供服务时,大量请求直接涌向数据库,可能导致数据库因负载过高而宕机的现象

处理缓存雪崩问题的策略通常包括以下几个方面:

  1. 保持缓存层的高可用性:可以通过部署Redis哨兵模式或Redis集群来提高缓存层的可用性,这样即使有个别节点失效,缓存层仍能正常工作。同时,在多个机房部署Redis也可以防止单个机房故障导致的缓存服务不可用。

  2. 使用限流降级组件:通过引入如Hystrix、Sentinel等限流降级组件,可以控制资源的并发访问量,防止资源过载,确保系统的稳定性。在缓存层或存储层出现问题时,能够及时进行降级处理,避免整个系统因此不可用。

  3. 优化缓存过期时间:为每个key设置合理的过期时间,避免大量key在同一时刻过期,导致缓存雪崩。可以通过在原有过期时间上增加一个随机值的方式来分散缓存失效时间,降低集体失效的风险。

  4. 使用互斥锁重建缓存:在高并发场景下,可以使用互斥锁来控制对存储层的查询和缓存重建过程,防止大量线程同时做相同的事情,减少对存储层的压力。

  5. 进行演练测试:在项目上线前,进行缓存层宕掉的模拟测试,评估应用和后端的负载情况,以及可能出现的问题,并制定相应的预案。

  • 29
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值