redis

Posted by zangxin on April 26, 2025

redis

文档

英文官方文档https://redis.io/docs/latest/commands/

中文文档 https://redis.com.cn/commands.html

redis介绍

为什么需要redis

  • 关系型数据库mysql性能瓶颈: 磁盘I/O性能低下 扩展瓶颈: 数据关系复杂, 扩展性差, 不便于大规模集群

  • redis是内存数存储, 降低I/O次数 不存储关系, 仅存储数据(没有表结构, 也不能用sql)

  • 用于缓存

    • 热点数据, 需要高性能访问
  • 分布式数据共享

    • 分布式集群架构中的session分离
  • 分布式锁

  • 消息队列

  • lua脚本

redis特点

  • 内存, 键值对存储

    • key是字符串, value可以有多种类型

redis安装配置

在linux系统安装

  • yum install gcc 安装包自己搞 cd /opt tar zxvf redis-6.2.6.tar.gz cd /opt/redis-6.2.6

    安装

    make install

  • 安装完成后,可执行文件在 /usr/local/bin 把配置文件copy到/etc/目录下 cp /opt/redis-6.2.6/redis.conf /etc/ 编辑配置,修改后台运行daemonize yes vi redis.conf 在/usr/local/bin目录启动redis-server ./redis-server /etc/redis.conf

    • 指令介绍

      • redis-benchmark:性能测试工具,可以在自己机器运行,看看自己机器性能如何 redis-check-aof:修复有问题的 AOF 文件, redis-check-dump:修复有问题的 dump.rdb 文件 redis-sentinel:Redis 集群使用 redis-server:Redis 服务器启动命令 redis-cli:客户端,操作入口
  • redis客户端 默认登录(本机6379) ./redis-cli 带端口登录 redis-cli -p 6379

    • 关闭redis-server

      • redis-cli shutdown redis-cli -p 6379 shutdown 在redis-cli shell中输入shutdown
  • 放在/usr/local/bin加入了环境变量中, 可以不用带绝对路径名执行命令

修改redis-server端口

  • vi redis.conf port 6379

redis指令

redis-cli基础指令

  • set

    • 设置k1的值为v1, 如果有k1,则覆盖 默认v1是字符串, 引号可加可不加(有特殊字符,如:空格,则必须加) set k1 v1
  • get

    • 获取set的值 get k1
  • clear 清除屏幕

  • quit/exit

    • 退出客户端, 不关闭redis-server
  • help 帮助

    • 举例: 查看get命令的使用说明 help get

对key的指令

  • keys *

    • 查看所有的key
  • 可以模糊匹配 keys *1 ,匹配以1结尾的所有key

  • exists

    • exists k1 查看k1是否存在

      • 存在返回1,不存在返回0
  • type

    • 查看k1的类型 type k1

      • 存在返回对应类型(string) 不存在返回none
  • del

    • 删除 del k1

      • 删除成功返回1 没有删除返回0
  • unlink

    • 类似于del删除key 但非阻塞删除(仅将key从keyspace元数据中删除, 真正的删除释放内存会重写开一个线程异步操作)
  • expire

    • 设置过期时间, k1 在20秒后失效 expire k1 20

      • 设置成功返回1,失败0
  • ttl

    • 查看是否过期时间 ttl k1

      • 过期/不存在返回-2 永不过期返回-1 没有过期返回还剩多久过期的时间(单位秒)

库操作

  • redis安装后默认有16个库(0-15), 启动时默认是0号库, 不同之间的库的数据是隔离的

  • select

    • 切换到1号库 select 1
  • dbsize

    • 查看当前库的key的数量
  • flushdb

    • 清空当前库,危险操作(删库)
  • flushall

    • 清空所有库,危险操作(删库)

redis数据类型

redis数据存储格式

  • 一句话: redis自身是一个Map, 其中所有数据都采用key-value形式存储

  • key是字符串, value是数据,数据支持多种类型/结构

string

  • 定义

    • String 是 Redis 最基本的类型,一个 key 对应一个 value

    • String 类型是二进制安全的, Redis 的 string 可以包含任何数据。比如 jpg 图片或者序列化 的对象。

    • String 类型是 Redis 基本的数据类型,一个 Redis 中字符串 value 最多可以是 512M

  • 指令

    • set 添加键值对

    • get 查询对应键值

    • append 将给定的 追加到原值的末尾

      • 返回字符串长度
    • strlen 获得值的长度

    • setnx 只有在 key 不存在时 设置 key 的值

    • decr 将 key 中储存的数字值(字符串)减 1 只能对数字值操作,如果为空,新增值为-1

    • incr 将 key 中储存的数字值(字符串)增 1, 只能对数字值操作,如果为空,新增值为1

    • incrby / decrby <步长>将 key 中储存的数字值增减。自定义步长, 如果为空,新增值.

      • incrby k3 10

        • 返回增加后的结果
    • mset ....., 同时设置一个或多个 key-value 对

    • mget ..... 同时获取一个或多个 value

    • msetnx ..... 同时设置一个或多个 key-value 对,当且仅当所 有给定 key 都不存在, 原子性,有一个失败则都失败

    • getrange <起始位置><结束位置> , 获得值的范围,(闭区间,从0开始)类似 java 中的 substring

      • k1=00000aa GETRANGE k1 -3 -1 倒数第三个到倒数第一个GETRANGE k1 0 2 第0个到第三个

      • 截取字符串 key=”hello world” 取出hello getrange key 0 4

      • 看来是支持切片(类似于python中数组) getrange k1 0 -1 取出整个value, 索引-1表示最后一个元素, -2 表示倒数第二个

    • setrange <起始位置> 覆写所储存的字符串值,从<起始位置> 开始(索引从 0 开始)。

      • 替换字符串 把word替换成 redis key=”hello world” SETRANGE k1 6 redis
    • setex <过期时间> 设置键值的同时,设置过期时间,单位秒。

    • getset , 以新换旧,设置了新值同时返回旧值

list

  • Map<String,List> 双向链表

    • 一句话: list 类型, 保存多个数据,底层使用双向链表存储结构实现

      • Node { Node pre Node next T data }

    • Redis 列表是简单的字符串列表,按照插入顺序排序。 你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

    • 底层是个双向链表,对两端的操作性能高,通过索引下标的操作中间的节点性能较差

    • 元素可以重复的

  • 指令

    • lpush/rpush .... 从左边/右边插入一个或多个值,如果key不存在,则新建一个list, 并加入元素

      • 规定一下

        • 左边是头, 右边是尾巴 用作队列, rpush插入到尾部, lpop从队头取出数据 用作栈,lpush入栈, lpop出栈
      • rpush l1 1 2 3 4 => l1 =[1,2,3,4]

    • lpop/rpop 从左边/右边吐出一个值, 结果:删除并返回, 且队列长度减一, 类似于removeFirst()

    • lrange 按照索引下标获得元素(从左到右) 索引可以为负数

    • lrange mylist0-1 0左边第一个,-1右边第一个,(0 -1表示获取所有value, 遍历所有value)

    • rpoplpush 列表右边吐出一个值,插到列表左边

    • 根据索引获取元素list.get(index) lindex

      • lindex l1 -1 最后一个元素 lindex l1 0 第一个元素 lindex l1 1 第二个元素
    • 将列表key下标为index的值替换成value lset

    • 获取列表长度 llen

    • 的前面/后面插入插入值 linsert BEFORE|AFTER

    • 从左边删除n个value(从左到右) lrem

  • list最佳实践

    • redis应用于操作有先后顺序的数据控制

    • 系统通知,按照时间顺序展示,将最近的通知列在前面

set

  • Map<String, Set> 一句话: set的功能于list是一个列表的功能, 但是元素不可以重复, 添加顺序和取出顺序不一致

    • 底层是dict字典,是哈希表的数据结构
  • 指令

    • sadd ..... 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略. 如果key不存存在,新建set并加入元素

    • 判断member是否是key中的子元素 SISMEMBER

    • smembers取出该集合的所有值。

    • 返回集合中元素个数(基数) scard 空集/不存在的key返回值为0

    • srem .... 删除集合中的某个元素。

    • spop随机从该集合中吐出一个值。

    • srandmember随机从该集合中取出n个值。不会从集合中删除。

    • smovevalue把集合中一个值从一个集合移动到另一个集合

    • 交集 sinter返回两个集合的交集元素

    • 并集 sunion返回两个集合的并集元素

    • 差集 sdiff返回两个集合的差集元素(key1中的,不包含key2中的)

hash

  • 一句话: Redis hash 是一个键值对集合,hash 适合用于存储对象, 类似 Java 里面的 Map<String,Object>

  • 指令

    • 集合中的 键赋值 (给对象设置属性) hset 如果hash表不存在,那么新建并执行hset, 如果域存在,那么执行更新value操作, 返回值为整数, 更新的filed数量(影响行数于mysql)

      • hset user:1001 id 1001 name zangxin age 20 gender male
    • 集合取出 value hget

    • 批量设置 hash 的值 hmset ...

    • 批量取出 hash 的 filed 值 hmget ...

    • 查看哈希表 key 中,给定域 field 是否存在,存在时返回1 ,不存在时返回0 hexists

    • hkeys列出该hash集合的所有field

    • hvals列出该hash集合的所有value

    • 为哈希表 key 中的域 field 的值加上增量 hincrby

      • field的值类型必须为整数 increment可以为负数 如果field不存在,则新建,并初始化值(increment)
    • hsetnx 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在

sorted_set/zset

  • 说明

      1. Redis 有序集合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合。
      1. 不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照 从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复 了。
      1. 因为元素是有序的, 所以也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素
      1. 访问有序集合的中间元素也是非常快的, 你能够使用有序集合作为一个没有重复成员 的列表。
  • 指令

    • zadd ... 将一个或多个 member 元素及其 score 值加入 到有序集 key 当中。 score是整数或者双精度浮点数

      • zadd dota2_hero 1 dushe 2 feiji 3 xiaoniu 4 xiaohei 5 zeous
    • zrange [WITHSCORES] 返回有序集 key 中,下标在之间的元素,带 WITHSCORES,可以让分数一起和值返回到结果集

      • 可以使用负的索引值

        ZRANGE dota2_hero 0 -1 WITHSCORES

    • ZREVRANGE [WITHSCORES] 同上,按索引倒序排

    • zscore 返回有序集 key 中,成员 member 的 score 值

      • ZSCORE dota2_hero feiji
    • zrangebyscore key min max [withscores] [limit offset count] 返回有序集 key 中,所有 score 值介于min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

      • 按分数而非索引排序
    • zrevrangebyscore key max min [withscores] [limit offset count] 同上,改为从大到小排列。

      • 注意max 写在前面, 因为是从大到小
    • 为元素的score加上增量(正负值都可以) zincrby

    • zrem 删除该集合下,指定值的元素

    • zcount统计该集合,分数区间内的元素个数

    • zrank返回该值在集合中的排名(索引),从0开始。

redis配置

博客参考

  • https://www.cnblogs.com/nhdlb/p/14048083.html#_label0

常规配置

  • daemonize

    • 是否为后台进程,设置为 yes 设置为 yes 后, 表示守护进程, 后台启动 设置为no会占用一个命令行窗口
  • pidfile

    • pidfile /var/run/redis_6379.pid

    • 存放 pid 文件的位置,每个实例会产生一个不同的 pid 文件, 记录 redis的进程号

  • loglevel

    • redis 日志分为 4 个级别,默认的设置为 notice, 开发测试阶段可以用 debug(日志内容 较多,不建议生产环境使用),生产模式一般选用 notice

    • redis 日志分为 4 个级别说明

      • debug:会打印出很多信息,适用于开发和测试阶段;

      • verbose(冗长的):包含很多不太有用的信息,但比 debug 要清爽一些;

      • notice:适用于生产模式;

      • warning : 警告信息;

  • logfile

    • logfile “” 就是说,默认为控制台打印,并没有日志文件生成

    • 可以为 redis.conf 的 logfile 指定配置项

      • 比如配置成 logfile /var/log/redis/redis.log

      • 如果提示日志文件 redis.log 不存在,创建一个该文件即可

  • 设定库的数量

    • 默认配置 databases 16 设定库的数量 默认 16,默认数据库为 0 号

    • 可以以使用 SELECT 命令在连接上指定数据库 id

  • 设置密码

    • 永久设置

      requirepass foobared

      去掉注释 改成,登录时就需要使用密码 requirepass root

    • 在redis-cli中输入 auth root 就能访问了

    • 临时设置(redis-cli命令行)

      • #设置 config set requirepass root #查看密码 config get requirepass #重启redis-cli生效

      • 在命令中设置密码,是临时的, 重启 redis 服务器,密码就还原了

      • 永久设置,需要在配置文件中进行设置

Units单位

  • 11 # 1k => 1000 bytes 12 # 1kb => 1024 bytes 13 # 1m => 1000000 bytes 14 # 1mb => 10241024 bytes 15 # 1g => 1000000000 bytes 16 # 1gb => 10241024*1024 bytes 配置大小单位,开头定义了一些基本的度量单位,只支持 bytes,不支持 bit 不区分大小写

include

  • 多实例的情况可以把公用的配置文件提取出来, 然后 include
    35 # include /path/to/local.conf 36 # include /path/to/other.conf

网络

  • bind

    • 默认情况 bind=127.0.0.1 只能接受本机的访问请求

    • 如果服务器是需要远程访问的,需要将其注释掉

      • #bind 127.0.0.1 -::1
  • protected-mode

    • 默认是保护模式

      • protected-mode yes
    • 如果服务器是需要远程访问的, 需要将 yes 设置为 no

  • port

    • 端口默认是6379

      • port 6379
  • timeout

    • 一个空闲的客户端维持多少秒会关闭,0 表示关闭该功能, 即永不超时.默认值是0 timeout 0
  • tcp-keepalive

    • tcp-keepalive 是对访问客户端的一种心跳检测,每隔 n 秒检测一次, 单位为秒

    • 如果设置为 0,则不会进行 Keepalive 检测,建议设置成 60秒

    • 为什么需要心跳检测机制

      • 1) TCP 协议中有长连接和短连接之分。短连接环境下,数据交互完毕后,主动释放连接;

      • 2) 长连接的环境下,进行一次数据交互后,很长一段时间内无数据交互时,客户端可能意外断开,这些 TCP 连接并未来得及正常释放,那么,连接的另一方并不知道对端的情况, 它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消 耗和浪费,且有可能导致在一个无效的数据链路层面发送业务数据,结果就是发送失败。 所以服务器端要做到快速感知失败,减少无效链接操作,这就有了 TCP 的 Keepalive(保活 探测)机制

  • 细分主题 6

限制

  • maxclients

    • 设置 redis 同时可以与多少个客户端进行连接。

    • 默认为10000个客户端

      • maxclients 10000

    • 如果达到了此限制,redis会拒绝新的连接请求,并且向这些连接请求方发出”max number of clients reached”

  • maxmemory

    • 在默认情况下, 对 32 位 实例会限制在 3 GB, 因为 32 位的机器最大只支持 4GB 的 内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位机器限制最大 3 GB 的 可用内存是非常合理的,这样可以避免因为内存不足而导致 Redis 实例崩溃

    • 在默认情况下, 对于 64 位实例是没有限制

    • 当用户开启了 redis.conf 配置文件的 maxmemory 选项,那么 Redis 将限制选项的值 不能小于 1 MB

      • maxmemory

    • 1、Redis的maxmemory设置取决于使用情况, 有些网站只需要32MB,有些可能需要12GB。

    • 设置redis内存建议

      • 如果使用 Redis 做数据库的话,设置到物理内存的 1/2 到 3/4 左右都可以

      • Redis 的最大使用内存跟搭配方式有关,如果只是用 Redis 做纯缓存, 64-128M 对一般小 型网站就足够了

      • maxmemory 只能根据具体的生产环境来调试,不要预设一个定值,从小到大测试, 基本标准是不干扰正常程序的运行。

      • 如果使用了快照功能的话,最好用到 50%以下,因为快照复制更新需要双倍内存空间, 如果没有使用快照而设置 redis 缓存数据库,可以用到内存的 80%左右,只要能保证 Java、 NGINX 等其它程序可以正常运行就行了

  • maxmemory-policy

    • 当redis内存满了之后, 采取的策略

    • policy 一览

      • 1) volatile-lru:使用 LRU 算法移除 key,只对设置了过期时间的键;(最近最少使用)

      • 2) allkeys-lru:在所有集合 key 中,使用 LRU 算法移除 key

      • 3) volatile-random:在过期集合中移除随机的 key,只对设置了过期时间的键

      • 4) allkeys-random:在所有集合 key 中,移除随机的 key

      • 5) volatile-ttl:移除那些 TTL 值最小的 key,即那些最近要过期的 key

      • 6) noeviction:不进行移除。针对写操作,只是返回错误信息(默认)

  • maxmemory-samples

    • 默认值

      maxmemory-samples 5

    • 设置样本数量,LRU 算法和最小 TTL 算法都并非是精确的算法,而是估算值,所以你可 以设置样本的大小,redis 默认会检查这么多个 key 并选择其中 LRU 的那个

    • 一般设置 3 到 7 的数字,数值越小样本越不准确,但性能消耗越小

发布和订阅/消息队列

一句话:Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息, 订阅者 (sub) 接收消息

  • 当给这个频道发布消息后,消息就会发送给订阅的客户端

消息队列模型

  • 与任务队列进行交互的实体有两类

    • 生产者将需要处理的任务放入任务队列中

    • 消费者则不断地从任务队列 中读入任务信息并执行

  • Subscriber:收音机,可以收到多个频道,并以队列方式显示

  • Publisher:电台,可以往不同的 FM 频道中发消息

  • Channel:不同频率的 FM 频道

  • 从 Pub/Sub 的机制来看,它更像是一个广播系统,多个订阅者(Subscriber)可以订阅多个频道(Channel),多个发布者(Publisher)可以往多个频道(Channel)中发布消息。

发布订阅模式分类

  • 一个发布者,多个订阅者

    • 主要应用:通知、公告 可以作为消息队列或者消息管道

  • 多个发布者,一个订阅者

    • 各应用程序作为 Publisher 向 Channel 中发送消息,Subscriber 端收到消息后执行相应的 业务逻辑,比如写数据库,显示

    • 主要应用:排行榜、投票、计数

  • 多个发布者,多个订阅者

    • 可以向不同的 Channel 中发送消息,由不同的 Subscriber 接收。

    • 主要应用:群聊、聊天

命令行实现发布和订阅

  • 发布订阅操作

    • 1、PUBLISH channel msg 将信息 message 发送到指定的频道 channel

      • 返回的值是订阅channel的订阅者数量
    • 2、SUBSCRIBE channel [channel …] 订阅频道,可以同时订阅多个频道

    • 3、UNSUBSCRIBE [channel …] 取消订阅指定的频道, 如果不指定频道,则会取消订阅所有频道

    • 4、PSUBSCRIBE pattern [pattern …]

      • 订阅一个或多个符合给定模式的频道,每个模式以 * 作为匹配符,比如 it* 匹配所 有 以 it 开头的频道( it.news 、 it.blog 、 it.tweets 等等), news.* 匹配所有 以 news. 开 头的频道( news.it 、 news.global.today 等等),诸如此类
    • 5、PUNSUBSCRIBE [pattern [pattern …]]

      • 退订指定的规则, 如果没有参数则会退订所有规则

注意

  • 发布的消息没有持久化

  • 订阅的客户端, 只能收到订阅后发布的消息

Jedis

Java 程序操作 Redis 的工具

文档

  • https://redis.io/docs/latest/develop/clients/jedis/

maven依赖

  • redis.clients jedis 5.2.0

</dependency>

允许redis远程连接

  • 注释掉bind 关闭保护模式 protectionmode no 开放linux的6379端口

连接

  • // 连接redis Jedis jedis = new Jedis(“192.168.2.85”, 6379); jedis.auth(“root”); String res = jedis.ping(); System.out.println(“res = “ + res); // pong // 关闭连接 jedis.close();

操作key

  • jedis.set(“k1”, “v1”); jedis.set(“k2”, “v2”); jedis.set(“k3”, “v3”); Set keys = jedis.keys("*"); keys.forEach(System.out::println);

System.out.println(jedis.exists(“k1”));// true System.out.println(jedis.ttl(“k2”));// -1 System.out.println(jedis.get(“k3”)); // v3

操作string

  • jedis.mset(“k1”, “vv1”, “k2”, “vv2”, “k3”, “vv3”,”k4”, “vv4”); List stringKeyList = jedis.mget("k1", "k2", "k3", "k4"); stringKeyList.forEach(System.out::println);

操作list

  • // 添加数据 jedis.lpush(“name_list”, “tom”, “jerry”, “zangxin”, “alice”); // 取出数据 List nameList = jedis.lrange("name_list", 0, -1); nameList.forEach(System.out::println);

操作set

  • jedis.sadd(“heroes”,”dushe”,”xiaohei”,”feiji”,”zeous”,”luna”); Set heroes = jedis.smembers("heroes"); heroes.forEach(System.out::println);

操作hash

  • jedis.hset(“zangxin”, “name”, “zangxin”); jedis.hset(“zangxin”, “age”, “20”); jedis.hset(“zangxin”, “gender”, “male”); jedis.hset(“zangxin”, “game”, “dota2”);

String game = jedis.hget(“zangxin”, “game”); System.out.println(game);

// 第二种放入方式–>map HashMap<String, String> map = new HashMap<>(); map.put(“name”, “zangxin”); map.put(“age”, “22”); jedis.hset(“zangxin2”, map);

List values = jedis.hmget("zangxin", "name", "age", "gender", "game"); values.forEach(System.out::println);

操作zset

  • jedis.zadd(“role”, 1, “桐人”); jedis.zadd(“role”, 3, “亚丝娜”); jedis.zadd(“role”, -1, “诗乃”); jedis.zadd(“role”, 2, “莉法”);

List role = jedis.zrange("role", 0, -1); role.forEach(System.out::println);// 诗乃,桐人,莉法,亚丝娜 // 逆序 List reverse = jedis.zrevrange("role", 0, -1); reverse.forEach(System.out::println);// 亚丝娜,莉法,桐人,诗乃

springboot整合redis

看springboot的内容

redis持久化

把redis存在内存中数据写入到磁盘中

持久化的策略有

  • RDB (RedisDataBase)

    • 在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是Snapshot 快照,恢复时将快照文件读到内存

    • RDB执行流程

        • 1) redis 客户端执行 bgsave 命令或者自动触发 bgsave 命令;

        • 2) 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;

        • 3) 如果不存在正在执行的子进程,那么就 fork 一个新的子进程进行持久化数据,fork 过程 是阻塞的,fork 操作完成后主进程即可执行其他操作;

          • Fork&Copy-On-Write

            • 1、Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、 程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程

              1. 在 Linux 程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了”写时复制技术 即: copy-on-write” ,
            • https://blog.csdn.net/Code_beeps/article/details/92838520
        • 4) 子进程先将数据写入到临时的 rdb 文件中,待快照数据写入完成后再原子替换旧的 rdb文件

        • 5) 同时发送信号给主进程,通知主进程 rdb 持久化完成,主进程更新相关的统计信息

      • 1) 整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能

      • 2) 如果需要进行大规模数据的恢复, 且对于数据恢复的完整性不是非常敏感,那 RDB 方式 要比 AOF 方式更加的高效

      • 3) RDB 的缺点是最后一次持久化后的数据可能丢失

        • -如果你是正常关闭 Redis , 仍然会进行持久化, 不会造成数据丢失

        • -如果是 Redis 异常终止/宕机(kill -9), 就可能造成数据丢失

    • dump.rdb配置

      • redis.conf的SNAPSHOTTING部分

        • ################################ SNAPSHOTTING ################################
      • 在redis配置文件redis.conf的433行

        • 433 dbfilename dump.rdb

        • 456 dir ./

          • 指定文件保存的目录

            • 可以指定为绝对路径, 不管在哪里启动, 读取的rdb.dump都是同一个 dir /root/
        • 这个默认文件在你启动redis-server的目录下 我是root启动的, 所以在/root/dump.conf

      • 默认: 在关闭时保存

        • 在redis-cli输入 shutdown save或者不写save会自动保存rdb 不想保存写 shutdown nosave
      • 自动保存: 默认快照配置

        • 383 # save 3600 1 384 # save 300 100 385 # save 60 10000

          • After 3600 seconds (an hour) if at least 1 key changed After 300 seconds (5 minutes) if at least 100 keys changed After 60 seconds if at least 10000 keys changed

          • 说明 save 60 10000

            • 每60秒进行key变化统计 60秒结束后, 下一个60秒为新的统计时间段
          • 如果我们没有开启 save 的注释, 那么在退出 Redis 时, 也会进行备份, 更新 dump.db

      • stop-writes-on-bgsave-error

        • 当 Redis 无法写入磁盘的话(比如磁盘满了), 直接关掉 Redis 的写操作。推荐 yes
      • rdbcompression

        • 对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis 会采用 LZF 算法进行压缩 如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能, 默认 yes
      • rdbchecksum

        • 在存储快照后, 还可以让 redis 使用 CRC64 算法来进行数据校验,保证文件是完整的

        • 但是这样做会增加大约 10%的性能消耗,如果希望获取到最大的性能提升,可以关闭 此功能, 推荐 yes

    • 手动保存

      • save

        • 1、save :save 时只管保存,其它不管,全部阻塞。手动保存, 不建议。

        • 格式:save 秒钟 写操作次数

          • 383 # save 3600 1
        • RDB 是整个内存的压缩过的 Snapshot,RDB 的数据结构,可以配置复合的快照触发条件.同时配置好几个save 秒钟 key改变次数

      • bgsave

        • 2、bgsave:Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
      • lastsave

        • 3、可以通过 lastsave 命令获取最后一次成功执行快照的时间(unix 时间戳) , 可以使用工具转换
      • 动态停止 RDB

        • redis-cli config set save “”

        • 说明: save 后给空值,表示禁用保存策略 这是临时的,重启失效, 如果想要永久生效可以在 在redis.conf配置 save “”

      • flushall(慎用=删库)

        • 1、执行 flushall 命令,也会产生 dump.rdb 文件, 数据为空. 备份也没有了

        • 2、Redis Flushall 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key)

    • RDB备份与恢复

      • Redis 可以充当缓存, 对项目进行优化, 因此重要/敏感的数据建议在 Mysql 要保存一份

      • 从设计层面来说, Redis 的内存数据, 都是可以重新获取的(可能来自程序, 也可能来自 Mysql)

      • Redis 启动时, 初始化数据是从 dump.rdb 来的,

      • 在redis-cli中查看备份文件位置

        • config get dir
      • 定时备份 dump.rdb

        • dump_2025_4_9.rdb
      • 恢复

        • 把dump_2025_4_9.rdb复制成dump.rdb 重启redis服务
    • RDB的优势

      • 1、适合大规模的数据恢复

      • 2、对数据完整性和一致性要求不高更适合使用

      • 3、节省磁盘空间

      • 4、恢复速度快

        • 截屏2025-04-09 15.39.50.png
    • RDB缺点

        1. 尽管redis在fork 时使用了写时拷贝技术(Copy-On-Write), 但是如果数据庞大时还是比较消耗性能。
      • 2、在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话(如果正常 关闭 Redis, 仍然会进行 RDB 备份, 不会丢失数据), 就会丢失最后一次快照后的所有修改
  • AOF(Append Only File)

    • 定义

      • 以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来(比如 set/del 操作会记录, 读操作 get 不记录)

      • 只许追加文件但不可以改写文件

      • redis 启动之初会读取该文件重新构建数据

      • redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

    • AOF持久化流程

      • 1、持久化流程示意图

          • 1) 客户端的请求写命令会被 append 追加到 AOF 缓冲区内

          • 2) AOF 缓冲区根据 AOF 持久化策略[always,everysec,no]将操作 sync 同步到磁盘的 AOF 文件

          • 3) AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容 量

          • 4) Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的

    • AOF开启

      • 0, 开启: 在redis.conf中 1256 appendonly no 把no改成yes

      • 1、在 redis.conf 中配置文件名称,默认为 appendonly.aof

      • 2、AOF 文件的保存路径,同 RDB 的路径一致。

      • 3、AOF 和 RDB 同时开启,系统默认取 AOF 的数据

    • AOF启动/修复/恢复

      • 基本说明

        • AOF 的备份机制和性能虽然和 RDB 不同, 但是备份和恢复的操作同 RDB 一样, 都是拷贝备 份文件, 需要恢复时再拷贝到 Redis 工作目录下,启动系统即加载
      • 正常恢复

        • 1、修改默认的 appendonly no,改为 yes

        • 2、将有数据的 aof 文件定时备份, 需要恢复时, 复制一份保存到对应目录(查看目录:config get dir)

        • 3、恢复:重启 redis 然后重新加载

      • 异常恢复

        • 1、如遇到 AOF 文件损坏, 通过/usr/local/bin/redis-check-aof –fix appendonly.aof 进行恢复

        • 2、建议先: 备份被写坏的 AOF 文件

          • 可能造成数据丢失
        • 3、恢复:重启 redis,然后重新加载

    • 同步频率设置

      • 每条写指令都同步一次 appendfsync always

        • 始终同步,每次 Redis 的写入都会立刻记入日志;性能较差但数据完整性比较好
      • 每秒同步一次(默认) appendfsync everysec

        • 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
      • appendfsync no(不推荐)

        • redis 不主动进行同步,把同步时机交给操作系统
    • Rewrite压缩

      • 1、rewrite 重写介绍

        • 1) AOF 文件越来越大,需要定期对 AOF 文件进行重写达到压缩

        • 2) 旧的 AOF 文件含有无效命令会被忽略,保留最新的数据命令 , 比如 set a a1 ; set a b1 ; set a c1; 保留最后一条指令就可以了

        • 3) 多条写命令可以合并为一个 , 比如 set a c1 b b1 c c1;

        • 4) AOF 重写降低了文件占用空间

        • 5) 更小的 AOF 文件可以更快的被 redis 加载

      • 2、重写触发配置

        • 1) 手动触发

          • 直接调用 bgrewriteaof 命令
        • 2) 自动触发

          • auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

            • auto-aof-rewrite-min-size: AOF 文件最小重写大小, 只有当 AOF 文件大小大于该值时候才能 重写, 默认配置 64MB

            • auto-aof-rewrite-percentage: 当前 AOF 文件大小和最后一次重写后的大小之间的比率等于 或者大于指定的增长百分比,如 100 代表当前 AOF 文件是上次重写的两倍时候才重写

            • 系统载入时或者上次重写完毕时,Redis 会记录此时 AOF 大小,设为 base_size, 如果 Redis 的 AOF 当前大小>= base_size +base_size*100% (默认)且当前 大小>=64mb(默认)的情况下,Redis 会对 AOF 进行重写

      • 3.重写后就无法查看redis命令内容了

    • 优点

      • 1、备份机制更稳健,丢失数据概率更低。

      • 2、可读的日志文本,通过操作 AOF 稳健,可以处理误操作
    • 缺点

      • 1、比起 RDB 占用更多的磁盘空间

      • 2、恢复备份速度要慢

      • 3、每次读写都同步的话,有一定的性能压力

  • No persistence

  • RDB+AOF

选择持久化策略

  • https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/

  • 官方推荐官方推荐两个都启用

    • 如果您希望获得与 PostgreSQL 相当的数据安全性,那么这两种持久化方法都适用。如果您非常重视数据,但又能忍受灾难发生时几分钟的数据丢失,那么您可以单独使用 RDB 持久化。许多用户单独使用 AOF 持久化,但我们不建议这样做,因为不时创建 RDB 快照对于数据库备份、快速重启以及 AOF 引擎出现 bug 时非常有用。
  • 如果只做缓存

    • 如果你只希望你的数据在服务器运行的时候存在, 你也可以不使用任何持久化方式

redis事务-解决超卖问题

定义

  • 1、Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行

  • 2、事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

  • 3、Redis 事务的主要作用就是串联多个命令防止别的命令插队

Redis事务三特性

  • 单独的隔离操作

    • 1、事务中的所有命令都会序列化、按顺序地执行

    • 2、事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

  • 没有隔离级别的概念

    • 队列中的命令(指令), 在没有提交前都不会实际被执行
  • 不保证原子性

    • 事务执行过程中, 如果有指令执行失败,其它的指令仍然会被执行, 没有回滚

事务指令

  • 事务指令示意图

  • multi

    • 从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行(类似 Mysql的 start transaction 开启事务)
  • exec

    • 输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行(类似 Mysql 的 commit 提交事务)
  • discard

    • 组队的过程中可以通过 discard 来放弃组队(类似 Mysql 的 rollback 回滚事务)

案例/注意点

  • 需求: 请依次向 Redis 中, 添加三组数据, k1-v1 k2-v2 k3-v3, 要求使用 Redis 的事务完成

  • 实现: 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set tx1 x1 QUEUED 127.0.0.1:6379(TX)> set tx2 x2 QUEUED 127.0.0.1:6379(TX)> set tx3 x3 QUEUED 127.0.0.1:6379(TX)> exec 1) OK 2) OK 3) OK

    • 全部成功
  • 放弃组队 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set tx1 x1 QUEUED 127.0.0.1:6379(TX)> set tx2 x2 QUEUED 127.0.0.1:6379(TX)> DISCARD OK 127.0.0.1:6379>

  • 组队阶段发生错误 127.0.0.1:6379(TX)> set tx1 x1 QUEUED 127.0.0.1:6379(TX)> set tx2 (error) ERR wrong number of arguments for ‘set’ command 127.0.0.1:6379(TX)> set tx3 x3 QUEUED 127.0.0.1:6379(TX)> exec (error) EXECABORT Transaction discarded because of previous errors.

    • 因为组队阶段错误,导致exec执行事务时, 事务失败, 所有指令都不会被执行,这个情况是有原子性的
  • 一部分成功一部分失败 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set tx1 x1 QUEUED 127.0.0.1:6379(TX)> set tx2 x2 QUEUED 127.0.0.1:6379(TX)> INCRBY tx2 1 QUEUED 127.0.0.1:6379(TX)> set tx3 x3 QUEUED 127.0.0.1:6379(TX)> exec 1) OK 2) OK 3) (error) ERR value is not an integer or out of range 4) OK 127.0.0.1:6379>

    • 如果组队成功, 但是指令有不能正常执行的, 那么 exec 提交, 会出现有成功有失败情况,也就是事务得到部分执行, 这种情况下, Redis 事务不具备原子性.

redis事务冲突解决方案

  • 问题提出 经典的抢票问题

    • 1) 一个请求想购买 6 2) 一个请求想购买 5 3) 一个请求想购买 1

      • 1) 如果没有控制, 会造成超卖现象 2) 如果 3 个指令, 都得到执行, 最后剩余的票数是 -2
  • 悲观锁

    • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人/其它请求想拿这个数据就会 block 直到它拿到锁。 悲观锁是锁设计理念, 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁, 表锁等,读锁,写锁等,都是在做操作之前先上锁.
  • 乐观锁

    • 乐观锁在进行版本修改时, 是需要原子性的

    • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。 乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种 check-and-set 机制实现事务的

  • watch & unwatch

    • 基本语法: watch key [key …]

    • 在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断.

    • unwatch

      • 取消 watch 命令对所有 key 的监视

      • 如果在执行 watch 命令后,exec 命令或 discard 命令先被执行了的话,那么就不 需要再执行 unwatch 了

  • 乐观锁演示

    • client A

      • 127.0.0.1:6379> set lock 100 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set share_data ggstar QUEUED 127.0.0.1:6379(TX)> incr lock QUEUED 127.0.0.1:6379(TX)> exec 1) OK 2) (integer) 101
    • client B

      • 127.0.0.1:6379> get lock “100” 127.0.0.1:6379> watch lock OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set share_data mmstar QUEUED 127.0.0.1:6379(TX)> incr lock QUEUED 127.0.0.1:6379(TX)> exec (nil) 执行失败嘞, 原因是lock版本已经更新了
    • 先执行 client A的exec,再执行client B的exec发现B失败了

    • 解释: A连接和B连接都操作redis 在事务开始前,都watch lock; 理解为获取lock的v1.0版本 A和B都开启multi事务,进行指令组队

    • A连接,先要提交事务exec, 这时可以理解redis底层乐观锁机制, 将lock的版本设置v1.1

    • B连接,再提交事务exec, 但是因为B原来获取的lock版本是v1.0,现在版本是v1.1就会造成事务被打断, 所以B连接的事务执行失败了

  • 火车票-抢票案例-java

    • 迭代1-实现功能

      • 代码

        • public boolean doSecKill(String userId, String ticketNo) { // 库存key String stockKey = “sk:” + ticketNo + “:ticket”; // 用户key, 购买的userId放入到这个set集合中 String userKey = “sk:” + ticketNo + “:user”; // 获取库存, 如果为null, 说明未配置库存, 直接返回 Jedis jedis = getRedis(); String stock = jedis.get(stockKey); if (StringUtils.isEmpty(stock)) { System.out.println(“活动还未开始, 库存还未配置”); release(jedis); return false; } // 判断用户是否重复购买 boolean isUserBuy = jedis.sismember(userKey, userId); if (isUserBuy) { System.out.println(userId + “ 已经参加过秒杀了”); release(jedis); return false; }

      // 判断库存是否充足 if (Integer.parseInt(stock) <= 0) { System.out.println(“已售罄…”); release(jedis); return false; }

      // 抢票扣减库存 jedis.decr(stockKey); // 将已经抢到的用户加入set中, 用来判断是否重复购买 jedis.sadd(userKey, userId); System.out.println(userId + “抢票成功”); return true; }

      • 分析

        • 存在超卖问题

          • 判断库存 扣减库存 并不是一个原子操作, 判断在前, 扣减在后面 如果有多个线程判断库存充足, 都进入下一步, 结果, 第一个线程扣减成功,恰好库存没了, 然后后面线程也会去扣减库存, 导致负数库存
    • 迭代2-解决超卖

      • 思路 redis事务的乐观锁

      • 代码

        • public boolean doSecKill(String userId, String ticketNo) { JedisPool jedisPool = JedisUtil.getInstance(); Jedis jedis = jedisPool.getResource();

      // 库存key String stockKey = “sk:” + ticketNo + “:ticket”; // 用户key, 购买的userId放入到这个set集合中 String userKey = “sk:” + ticketNo + “:user”;

      // 监视库存 jedis.watch(stockKey);

      String stock = jedis.get(stockKey); if (stock == null) { System.out.println(“秒杀还没有开始,请等待..”); jedis.close(); return false; }

      // 判断用户是否重复购买 boolean isUserBuy = jedis.sismember(userKey, userId); if (isUserBuy) { System.out.println(userId + “ 已经参加过秒杀了”); jedis.close(); return false; }

      // 判断库存是否充足 if (Integer.parseInt(stock) <= 0) { System.out.println(“已售罄…”); jedis.close(); return false; }

      // 开始事务 Transaction multi = jedis.multi(); multi.decr(stockKey); multi.sadd(userKey, userId); List result = multi.exec(); if (CollectionUtils.isEmpty(result)) { System.out.println(“抢票失败”); jedis.close(); return false; } return true; }

      • 分析存在的问题

        • 设置600张票, 1000个人抢票,结果还有剩余的票

          • ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.2.42:8080/seckill/seckill -c 并发量设置越大, 遗留的库存就越多
        • 客户端只抢一次的情况, 大部分会被乐观锁的版本号拦截住

          • watch key decr key 修改了版本号, 后面的线程都会失败
    • 迭代3-解决库存遗留问题+顺便解决超卖(最终方案)

      • 代码

        • public boolean doSecKill(String userId, String ticketNo) { JedisPool jedisPool = JedisUtil.getInstance(); Jedis jedis = jedisPool.getResource();

// 库存key String stockKey = “sk:” + ticketNo + “:ticket”; // 用户key, 购买的userId放入到这个set集合中 String userKey = “sk:” + ticketNo + “:user”;

String lua_script = null; try { lua_script = IOUtils.resourceToString(“seckill.lua”, Charset.forName(“utf8”), this.getClass().getClassLoader()); } catch (IOException e) { throw new RuntimeException(e); } // 将脚本加载到redis内存中 String sha1 = jedis.scriptLoad(lua_script); // evalsha是根据sha1码来执行缓存在服务器的脚本 Object result = jedis.evalsha(sha1, 2, userId, ticketNo); String resultStr = String.valueOf(result); boolean ret = false; switch (resultStr) { case “-1”: System.out.println(“活动未开始”); ret = false; case “0”: System.out.println(“已售罄”); ret = false; case “1”: System.out.println(“抢票成功”); ret = true; case “2”: System.out.println(“用户已经购买过了”); ret = false; } jedis.close(); return ret; }

1
2
3
4
5
6
7
8
	- seckill.lua

		- local userid = KEYS[1]; local ticketno = KEYS[2]; local stockKey = 'sk:' .. ticketno .. ':ticket'; local usersKey = 'sk:' .. ticketno .. ':user'; if stockKey == nil then
return -1 end local userExists = redis.call('sismember', usersKey, userid); if userExists ~= nil and tonumber(userExists) == 1 then
return 2; end local num = redis.call("get", stockKey); if tonumber(num) <= 0 then
return 0; else
redis.call("decr", stockKey);
redis.call("sadd", usersKey, userid); end

return 1

redis连接池-java

定义

  • 节省每次连接 redis 服务带来的消耗,把连接好的实例反复利用。

连接池参数

    • MaxTotal:控制一个 pool 可分配多少个 jedis连接实例,通过 pool.getResource()来获取; 如果赋值为-1,则表示不限制
  • -maxIdle:控制一个 pool 最多有多少个状态为 idle(空闲)的 jedis 实例

  • -MaxWaitMillis:表示当获取一个 jedis 实例时,最大的等待毫秒数,如果超过等待时间, 则直接抛 JedisConnectionException

  • -testOnBorrow:获得一个 jedis 实例的时候是否检查连接可用性(ping());如果为 true, 则得到的 jedis 实例均是可用的

连接池工具类

  • 依赖 commons-pool2 jedis

    • org.apache.commons commons-pool2

      redis.clients jedis
  • 工具类,单例模式

    • public class JedisUtil { // volatile // 1.线程的可见性: 当一个线程去修改一个共线变量时, 另一外一个线程可以读取到整儿修改的值, 而不是副本 // 2.禁止指令重排: 执行顺序和代码顺序一致 private static volatile JedisPool jedisPool = null;

    private JedisUtil() { }

    // 单例模式-懒汉式-双检锁DCL public static JedisPool getInstance() { if (jedisPool == null) { synchronized (JedisUtil.class) { if (jedisPool == null) { // 连接池配置 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); // 最大连接数量 jedisPoolConfig.setMaxTotal(200); // 最大空闲数量 jedisPoolConfig.setMaxIdle(32); // 连接时最大等待时间60s jedisPoolConfig.setMaxWaitMillis(60 * 1000); // 耗尽连接时bocked jedisPoolConfig.setBlockWhenExhausted(true); // 检查连接是否可用 jedisPoolConfig.setTestOnBorrow(true); jedisPool = new JedisPool(jedisPoolConfig, “192.168.2.85”, 6379); } } } return jedisPool; }

    // 释放redis连接资源 public static void release(Jedis jedis) { if (null != jedis) { // 如果jedis是从连接池中获取, 则close是把连接还给pool, 而不是销毁掉 jedis.close(); } } }

  • 使用 JedisPool jedisPool = JedisUtil.getInstance(); Jedis jedis = jedisPool.getResource();

lua脚本

定义

  • 1、Lua 是一个小巧的脚本语言,Lua 脚本可以很容易的被 C/C++ 代码调用,也可以反过来调用 C/C++的函数,Lua 并没有提供强大的库,一个完整的 Lua 解释器不过 200k,所以 Lua 不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

  • 2、很多应用程序、游戏使用 LUA 作为自己的嵌入式脚本语言,以此来实现可配置性、 可扩展性。

  • 3、将复杂的或者多步的 Redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复 连接 redis 的次数。提升性能。

  • 4、LUA 脚本是类似 Redis 事务,有一定的原子性,不会被其他命令插队,可以完成一 些 redis 事务性的操作

  • 5、Redis 的 lua 脚本功能,只有在 Redis 2.6 以上的版本才可以使用

  • 6、通过 lua 脚本解决争抢问题,实际上是 Redis 利用其单线程的特性,用任务队列的 方式解决多任务并发问题

lua脚本的原子性

  • demo

    • LUA 脚本是类似 Redis 事务,有一定的原子性,不会被其他命令插队,能完成 Redis事务性的操作

    • 通过 lua 脚本解决争抢问题,Redis 利用其单线程的特性,将请求形成任务队列, 从 而解决多任务并发问题

lua脚本语法

  • local userid=KEYS[1]; – 获取传入的第一个参数 local ticketno=KEYS[2]; – 获取传入的第二个参数 local stockKey=’sk:’..ticketno..’:ticket’; – 拼接 stockKey local usersKey=’sk:’..ticketno..:user; – 拼接 usersKey local userExists=redis.call(sismember,usersKey,userid); – 查看在 redis 的 usersKey set 中是否有该用户 if userExists ~= nil and tonumber(userExists)==1 then return 2; – 如果该用户已经购买, 返回 2 end local num= redis.call(“get” ,stockKey); – 获取剩余票数 if tonumber(num)<=0 then return 0; – 如果已经没有票, 返回 0 else redis.call(“decr”,stockKey); – 将剩余票数-1 redis.call(“sadd”,usersKey,userid); – 将抢到票的用户加入 set
    end return 1 – 返回 1 表示抢票成功

    • 细分主题 1
  • 文档参考:https://blog.csdn.net/qq_41286942/article/details/124161359

  • lua脚本在java中写, 在redis中执行

redis调用lua脚本

  • // 将脚本加载到redis内存中 String sha1 = jedis.scriptLoad(lua_script); // evalsha是根据sha1码来执行缓存在服务器的脚本 Object result = jedis.evalsha(sha1, 2, userId, ticketNo); String resultStr = String.valueOf(result);

redis主从复制

读写分离

示意图

    • 1) 上图描述了主机数据更新后, 自动同步到备机的 master/slaver 机制

    • 2) Master 以写为主,Slaver 以读为主

    • 3) 好处: 读写分离, 提升效率 (理解: 读写分离后, 将读和写操作分布到不同的 Reids, 减少单个 Redis 的压力, 提升效率)

    • 4) 好处: 容灾快速恢复 (理解: 如果某个slaver, 不能正常工作, 可以切换到另一个slaver)

    • 5) 主从复制, 要求是 1 主多从, 不能有多个 Master( 理解: 如果有多个主服务器 Master, 那么 slaver 不能确定和哪个 Master 进行同步, 出现数据紊乱)

    • 6) 要解决主服务器的高可用性, 可以使用 Redis 集群

搭建一主多从

  • 公共配置

    • 拷贝之前的配置文件, 关闭aof redis.conf
  • 主服务:6379

    • 配置

      • include /zxredis/redis.conf pidfile /var/run/redis_6379.pid port 6379 dbfilename dump6379.rdb
  • 从服务:6380 从服务:6381

    • include /zxredis/redis.conf pidfile /var/run/redis_6380.pid port 6380 dbfilename dump6380.rdb

    • 从服务上执行, 选取主服务 SLAVEOF 127.0.0.1 6379

  • 查看角色是从服务,还是主服务 info replication

  • 启动 redis-server redis_6379.conf redis-server redis_6380.conf redis-server redis_6381.conf

  • 只能在主服务上进行写入操作, 在从服务写会报错, 写: 增加,删除,修改

主从复制原理

    • Slave 启动成功连接到 master 后会发送一个 sync 命令
    • Master 接到命令后, 启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在 后台进程执行完毕之后, master 将传送整个数据文件到 slave,以完成一次完全同步
    • slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中, 即 全量复制
    • Master 数据变化了, 会将新的收集到的修改命令依次传给 slave, 完成同步, 即 增量复制
    • 但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行

      • 宕机重启

一主二从

  • 如果从服务器 down 了, 重新启动(需要重新执行SLAVEOF 127.0.0.1 6379) , 仍然可以获取 Master 的最新数据

  • 如果主服务器 down 了, 从服务器并不会抢占为主服务器, 当主服务器恢复后, 从服务器仍然指向原来的主服务器.

  • 主6379 从: 6380 slaveof 127.0.0.1 6379 从: 6381 slaveof 127.0.0.1 6379

薪火相传

  • 1) 上一个 Slave 可以是下一个 slave 的 Master,Slave 同样可以接收其他 slaves 的连接和同 步请求,那么该 slave 作为了链条中下一个的 master, 可以有效减轻 master 的写压力,去中心 化降低风险

  • 2) 用 slaveof

    • 主6379 从: 6380 slaveof 127.0.0.1 6379 从: 6381 slaveof 127.0.0.1 6380
  • 3) 风险是一旦某个 slave 宕机,后面的 slave 都没法同步(馊主意)

  • 4) 主机挂了,从机还是从机,无法写数据了

反客为主

  • 在薪火相传的模式下,当一个 master 宕机后, 指向 Master 的 slave 可以升为 master, 其后面的 slave 不用做任何修改

  • 用 slaveof no one 将从机变为主机 (后面可以使用哨兵模式, 自动完成切换.)

哨兵模式(sentinel)

  • 哨兵模式(如图): 反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

  • 演示

    • 1) 调整为一主二仆模式,6379 带着 6380、6381 , 根据前面讲解的调整即可

    • 2) 创建 /zxredis/sentinel.conf , 名字不能乱写, 按照指定的来 sentinel.conf: sentinel monitor redis_master 127.0.0.1 6379 1 解释: -redis_master 为监控对象起的服务器名称 -1 表示至少有多少个哨兵同意迁移的数量, 这里我配置1 表示只要有1个哨兵同意迁移就 可以切换

    • 3) 启动哨兵, 注意看哨兵的端口是 26379 redis-sentinel sentinel.conf

    • 4) 当主机挂掉,从机选举中产生新的主机

    • 5) 如果原来的主机重启, 会自动成为从机

  • 在哨兵模式下,主机 down 后的执行流程分析

    • 解读上图 - 哨兵如何在从机中, 推选新的 Master 主机, 选择的条件依次为: 1) 优先级在 redis.conf 中默认:replica-priority 100,值越小优先级越高 2) 偏移量是指获得原主机数据的量, 数据量最全的优先级高 3) 每个 redis 实例启动后都会随机生成一个 40 位的 runid, 值越小优先级越高

redis集群

为什么需要集群-高可用性

  • 1、生产环境的实际需求和问题

      • 容量不够,redis 如何进行扩容?
      • 并发写操作, redis 如何分摊?
      • 主从模式,薪火相传模式,主机宕机,会导致 ip 地址发生变化,应用程序中配置需要修 改对应的主机地址、端口等信息
  • 2、传统解决方案-代理主机来解决

    • 1) 客户端请求先到代理服务器

    • 2) 由代理服务器进行请求转发到对应的业务处理服务器

    • 3) 为了高可用性, 代理服务、A 服务、B 服务、C 服务都需要搭建主从结构(至少是一主一从),这样就需求搭建至少 8 台服务器

    • 4) 这种方案的缺点是: 成本高,维护困难, 如果是一主多从, 成本就会更高.
  • 3、redis3.0 提供解决方案-无中心化集群配置

    • 1) 各个 Redis 服务仍然采用主从结构

    • 2) 各个 Redis 服务是连通的, 任何一台服务器, 都可以作为请求入口

    • 3) 各个 Redis 服务器因为是连通的, 可以进行请求转发

    • 4) 这种方式, 就无中心化集群配置, 可以看到,只需要 6 台服务器即可搞定

    • 5) 无中心化集群配置, 还会根据 key 值, 计算 slot , 把数据分散到不同的主机, 从而缓解单个主机的存取压力.

    • 6) Redis 推荐使用无中心化集群配置

    • 7) 在实际生成环境 各个 Redis 服务器, 应当部署到不同的机器(防止机器宕机, 主从复制失效)

集群

  • 1、Redis 集群实现了对 Redis 的水平扩容,即启动 N 个 redis 节点,将整个数据库分布存储在这 N 个节点中,每个节点存储总数据的 1/N。

  • 2、Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求

  • 搭建集群

    • 1.删除之前段*.rdb, *.aof文件

    • 2.redis cluster 配置修改

      • cluster-enabled yes 打开集群模式

      • cluster-config-file nodes-6379.conf 设定节点配置文件名

      • cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主 从切换

    • vi /hspredis/redis_6379.conf

      • include /zxredis/redis.conf pidfile “/var/run/redis_6379.pid” port 6379 dbfilename “dump6379.rdb” cluster-enabled yes cluster-config-file nodes-6379.conf cluster-node-timeout 15000
    • 复制5份

      • cp redis_6379.conf redis_6380.conf cp redis_6379.conf redis_6381.conf cp redis_6379.conf redis_6389.conf cp redis_6379.conf redis_6390.conf cp redis_6379.conf redis_6391.conf
    • 替换5个文件的端口

      • :%s/6379/6381/g
    • 启动6个redis服务

      • redis-server redis_6379.conf redis-server redis_6380.conf redis-server redis_6381.conf redis-server redis_6389.conf redis-server redis_6390.conf redis-server redis_6391.conf
    • 将六个节点合成一个集群

      • redis-cli –cluster create –cluster-replicas 1 192.168.2.85:6379 192.168.2.85:6380 192.168.2.85:6381 192.168.2.85:6389 192.168.2.85:6390 192.168.2.85:6391

        • Performing hash slots allocation on 6 nodes… Master[0] -> Slots 0 - 5460 Master[1] -> Slots 5461 - 10922 Master[2] -> Slots 10923 - 16383 [WARNING] Some slaves are in the same host as their master M: ff1dd3947e4a180775f09b7257ca84518fae27ef 192.168.2.85:6379 slots:[0-5460] (5461 slots) master M: 841e721bc0f6c2378303b7544ae49e4fb70c7511 192.168.2.85:6380 slots:[5461-10922] (5462 slots) master M: 9ade621ffcf8bf2e9fee7f511892ef35b2a560b3 192.168.2.85:6381 slots:[10923-16383] (5461 slots) master S: b70b04658b23780962d1b9c1965f5a83ce7f3f61 192.168.2.85:6389 replicates ff1dd3947e4a180775f09b7257ca84518fae27ef S: 4af8384eff0836cbfd1d0bedf5f55d7961c709a9 192.168.2.85:6390 replicates 841e721bc0f6c2378303b7544ae49e4fb70c7511 S: 070aa53b49980775f49b01c08a569bd7985f190d 192.168.2.85:6391 replicates 9ade621ffcf8bf2e9fee7f511892ef35b2a560b3

      • 注意

        • 1) 组合之前,请确保所有 redis 实例启动后,nodes-xxxx.conf 文件都生成正常

        • 2) 此处不要用 127.0.0.1, 请用真实 IP 地址, 在真实生产环境, IP 都是独立的.

        • 3) replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组

        • 4) 搭建集群如果没有成功, 把 sentinel 进程 kill 掉, 再试一下

    • 结果分成了三组(master-salve)

      • Slave Master slot 192.168.2.85:6389 to 192.168.2.85:6379 第一组 0-5460 192.168.2.85:6390 to 192.168.2.85:6380 第二组 5461-10922 192.168.2.85:6391 to 192.168.2.85:6381 第三组 10923-16383
    • 集群方式登录

      • redis-cli -c -p 6379

      • cluster nodes 命令查看集群信息, 主从的对应关系

    • 注意

      • 1、一个集群至少要有三个主节点

      • 2、选项 –cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。

      • 3、分配原则:尽量保证主服务器和从服务器各自运行在不同的 IP 地址(机器), 防止机器故 障导致主从机制失效, 高可用性得不到保障

  • Redis集群使用

    • slots

      • 集群启动时,会给master节点分配插槽(slot)

      • 一个 Redis 集群包含 16384(2^14) 个插槽(hash slot),编号从 0-16383, Reids 中的每个键都 属于这 16384 个插槽的其中一个

      • 3) 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句 用于计算键 key 的 CRC16 校验和

      • 4)集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中: -节点 A 负责处理 0 号至 5460 号插槽。 -节点 B 负责处理 5461 号至 10922 号插槽。 -节点 C 负责处理 10923 号至 16383 号插槽

    • 在集群中录入值

      • 1) 在 redis 每次录入、查询键值,redis 都会计算出该 key 应该送往的插槽,如果不是该客户 端对应服务器的插槽,redis 会告知应前往的 redis 实例地址和端口

        • [root@localhost zxredis]# redis-cli -c -p 6379 127.0.0.1:6379> set k1 v1 -> Redirected to slot [12706] located at 192.168.2.85:6381 OK 192.168.2.85:6381> get k1 “v1” 192.168.2.85:6381>
      • 2) redis-cli 客户端提供了 –c 参数实现自动重定向。

      • 3) 如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向

      • 4) 不在一个 slot 下的键值,是不能使用 mget,mset 等多键操作

        • 192.168.2.85:6381> mset k2 v2 k3 v3 k4 v4 (error) CROSSSLOT Keys in request don’t hash to the same slot
      • 5) 可以通过{}来定义组的概念,从而使 key 中{}内相同内容的键值对放到一个 slot 中去

        • 192.168.2.85:6381> mset k2{order} v2 k3{order} v3 k4{order} v4 OK 192.168.2.85:6381> keys * 1) “k1” 2) “k4{order}” 3) “k3{order}” 4) “k2{order}”
    • 查询集群中的值

      • CLUSTER KEYSLOT 返回 key 对应的 slot 值

        • 192.168.2.85:6381> CLUSTER KEYSLOT k2{order} (integer) 16025
      • CLUSTER COUNTKEYSINSLOT 返回 slot 有多少个 key

        • 192.168.2.85:6381> CLUSTER COUNTKEYSINSLOT 16025 (integer) 3
      • CLUSTER GETKEYSINSLOT 返回 count 个 slot 槽中的键

        • 192.168.2.85:6381> CLUSTER GETKEYSINSLOT 16025 3 1) “k2{order}” 2) “k3{order}” 3) “k4{order}”
  • Redis集群故障恢复

    • 1、如果主节点下线, 从节点会自动升为主节点(注意 15 秒超时, 再观察比较准确)

    • 2、主节点恢复后,主节点回来变成从机

    • 3、如果所有某一段插槽的主从节点都宕掉,Redis 服务是否还能继续, 要根据不同的配置而 言

      • 1) 如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为 yes ,那么 ,整个集 群都挂掉

      • 2) 如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为 no , 那么, 只是该插槽 数据不能使用,也无法存储

      • 3) redis.conf 中的参数 cluster-require-full-coverage

  • 集群的 Jedis 开发

    • 1、即使连接的不是主机,集群会自动切换主机存储。主机写,从机读。

    • 2、无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据

    • code

      • // 这里 set 也可以加入多个入口 // 因为没有配置日志, 会有提示, 但是不影响使用 // 如果使用集群,需要把集群相关的端口都打开, 否则会报 No more cluster attempts left // 4. JedisCluster 看源码有多个构造器, 也可以直接传入一个 HostAndPort HashSet hostAndPorts = new HashSet<>(); hostAndPorts.add(new HostAndPort("192.168.2.85", 6389)); JedisCluster jedisCluster = new JedisCluster(hostAndPorts); jedisCluster.set("name", "hyy"); String name = jedisCluster.get("name"); System.out.println("name = " + name);
  • Redis集群的优点

    • 1、实现扩容

    • 2、分摊压力

    • 3、无中心配置相对简单

  • 缺点

    • 1、多键操作是不被支持的

    • 2、多键的 Redis 事务是不被支持的。lua 脚本不被支持

    • 3、由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而其它方案想要迁移,至 redis cluster,需要整体迁移而不是逐步过渡,复杂度较大

缓存模型

请求==>redis缓存==>DB

缓存穿透

问题描述

  • 前端请求查询了一个数据库中不存在的数据。连数据库里面都没有,那么缓存中肯定更没有

  • 由于缓存穿透必然执行数据库查询,所以会增加数据库压力。严重时会把数据库压垮。

解决方案

  • 1.创建空对象存储缓存,设置较短的过期时间(因为正常情况下该情况发生几率很低),最长不超过5分钟。

  • 2.定义一个可以访问的白名单, 每次访问和白名单id进行比较, 如果id不在白名单里面,进行拦截, 不允许访问, 比如使用bitmaps

  • 3.采用布隆过滤器

    • 布隆过滤器可以用于检索一个元素是否在一个集合中, 他的优点是空间效率和查询时间都远远超过一般的算法, 缺点是有一定误识别率和删除困难
  • 4.进行实时监控

    • 当发现redis命中率开始急速降低, 需要排查访问对象和数据, 和运维配置, 设置黑名单, 限制访问服务

缓存击穿

key对应的数据存在于DB, 但是在redis过期了,有请求来访问时无法命中,需查询数据库

危害:如果该数据是热点数据,会导致针对这条数据的数据库访问激增,增加数据库压力

数据库访问压力瞬间增加, 但redis中没有大量key过期, 表面上redis正常运行,但是数据库可能瘫痪了

解决方案

  • 预先设置热门数据

    • 在redis高峰访问之前, 把一些热门数据提前存入到redis中, 加大这些热门数据key的时长
  • 实时调整

    • 现场监控哪些热门数据, 实时调整key过期时长
  • 使用锁

    • 在缓存失效时, 不是立即去loadDB 而是 setnx lock_flag 当操作成功时, 在进行loadDB, 回写缓存, 删除lock_flag 操作失败时, 说明有线程在loadDB, 让当前线程睡眠一段时间, 再重新get缓存

缓存雪崩

大批缓存数据同时失效,原本可以从缓存中获取数据的大量请求查询数据库,数据库压力激增

数据库压力激增,瞬间崩溃。

解决办法

  • 分散失效时间

    • 给缓存数据设置过期时间时,使用不同时间长度,避免大量数据同时失效。具体做法是可以给过期时间加随机数,例如1~5分钟随机。
  • 多级缓存

    • nginx缓存+redis缓存+其他缓存
  • 使用锁和队列

    • 用加锁或者队列的方式来保证不会有大量线程对数据库一次性进行读写.(不适合高并发)
  • 设置过期标志更新缓存

    • 记录缓存数据是否过期, 如如果过期触发通知另外的线程在后台去更新实际的key

分布式锁

问题描述

  • 随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

  • 拿到锁,各个子模块才能进行业务操作

主流实现方案

  • 基于数据库

  • 基于缓存redis

    • 性能最高
  • 基于zookeeper

    • 可靠性最好

基于redis实现分布式锁

    1. setnx key value 指令
    • 当且仅当key不存在时, 才能设置成功 相当于加锁
  • 2.del key 删除key

    • 相当于释放锁
    1. expire key seconds 设置过期时间
    • 给锁设置过期时间, 避免死锁
  • 4.ttl key 查看过期时间

    1. set key value nx ex seconds
    • 设置锁并同时设置过期时间 相当于两条指令, 但是具有原子性

基于redis实现分布式锁-java代码版本

  • 业务场景: 如果获取锁, 则对key=num的值, 在执行num+1的更新操作,并释放锁 如果没有获取到锁, 休眠100ms后,再尝试获取

  • code

    • @RestController public class RedisDistributedLock { @Autowired private RedisTemplate redisTemplate;

    @GetMapping(“/lock”) public String lock() throws InterruptedException { // 获取随机的uuid避免锁被误删除 String uuid = UUID.randomUUID().toString(); // 设置3秒的过期时间,3秒后自动释放锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent(“lock”, uuid, 3, TimeUnit.SECONDS); if (lock) { // true说明获取到锁 Object value = redisTemplate.opsForValue().get(“num”); // 没有值直接返回 if (!StringUtils.hasText(value.toString())) { return “failed”; } // 执行更新num+1的业务操作 int num = Integer.parseInt(value.toString()); redisTemplate.opsForValue().set(“num”, ++num); // 释放-lock,只有获取的锁和自己锁是同一把锁才删除 String atomicDelLua = “if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”; DefaultRedisScript redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(atomicDelLua); redisScript.setResultType(Long.class); Long result = (Long) redisTemplate.execute(redisScript, Arrays.asList("lock"), uuid); if (result.equals(1L)) { System.out.println("删除成功"); } // -这里删除操作缺乏原子性,使用lua脚本原子性 // if (uuid.equals(redisTemplate.opsForValue().get("lock"))) { // redisTemplate.delete("lock"); // } return "ok"; } else { // 没有获取到锁, 睡眠100ms Thread.sleep(100); // 递归调用自己 lock(); } return "ok"; } }

    • lua

      • if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end
  • 测试

    • ab -n 1000 -c 100 http://192.168.2.42:8080/seckill/lock
  • 注意事项

    • 定义锁的key,key可以根据业务,分别设置, 比如操作商品,key应该为每一个sku定义的, 也就是每个sku都有一把锁

    • 为了确保分布式锁可用, 要确保锁同时满足四个条件 互斥性: 在任意时刻, 只有一个客户端持有锁 不会发生死锁. 即使有一个客户端在持有锁的期间崩溃而没有主动释放锁, 也能保证后续其他客户端能加锁 加锁和解锁必须是同一个客户端, 客户端A不能把客户端B的锁给解了

    • 加锁和解锁必须具有原子性

redis ACL(redis 6)

access control list 访问控制列表

该功能可以限制执行的命令和可以访问的key

redis6 提供更加细粒度的权限控制

  • 接入权限: 用户名和密码 可以执行的命令 可以操作的key

acl list

  • 展现当前用户的权限列表 1) “user default on nopass ~* &* +@all”

    • default 用户名 on/off 是否启用

    • nopass 没有密码

    • ~* 可以操作的key

    • +@all 可以执行的命令

acl cat

  • 查看添加权限的类型

  • 查看用户能操作的string权限 acl cat string

acl whoami

  • 查看当前用户是谁

acl

  • 创建用户

    • acl set user tom

      • “user tom off &* -@all” 这样创建的用户不能使用,没有激活(默认创建)
    • acl setuser jack on >password ~cached:* +get

      • jack 用户名 on 启用

        password 表示密码就是password ~cached:*表示操作的key是以cache开头的 +get 表示操作的指令只能是get

        • “user jack on #5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 ~cached:* &* -@all +get”
  • 切换用户

    • auth jack password

      • 只能执行get cached:k1

      • 127.0.0.1:6379> set cached:k1 v1 (error) NOPERM this user has no permissions to run the ‘set’ command or its subcommand

  • 增加权限(切换到默认用户)

    • acl setuser jack +set

      • 给jack增加set权限
  • 删除用户

    • acl deluser jack

IO多线程

IO多线程是指和客户端交互时, 网络IO交互处理模块多线程,而非执行命令多线程

redis6执行命令依然是单线程

也就是说redis和客户端的交互是多线程, 在执行指令的时候,仍然是单线程+IO多路复用

IO多线程默认是不开启的, 需要在配置文件中设置

io-threads-do-reads no io-threads 4

cluster工具

  • 之前老版redis搭建集群需要ruby环境 redis5将redis-trib.rb功能集成到redis-cli redis-benchmark是一个性能测试工具, 可以开启集群压测功能

resp3新的redis通信协议, 优化服务端和客户端通信

客户端缓存,基于resp3协议实现, 提高性能,减少tcp网络交互

proxy集群代理模式, proxy功能, 让cluster拥有像单实例一样的接入方式, 降低使用cluster的门槛

modules api