Redis基础

  • 简介:Redis是一个开源的key-value存储系统。

  • 特点:

    • 数据都在内存中,支持持久化,主要用作备份恢复
    • 除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等。
    • 一般是作为缓存数据库辅助持久化的数据库

安装

  1. 官方下载安装包

  2. 上传安装包到/opt下

  3. 在linux下安装gcc环境:yum install -y gcc tcl

  4. 解压 tar -zxvf redis-6.2.1.tar.gz

  5. 进入redis目录执行make命令

  6. 执行make install 安装

  7. 安装目录在/usr/local/bin下

  8. 在安装包的目录下备份配置文件cp redis.conf redis.conf.bck

  9. 修改配置文件(最好不要直接修改原文件,复制到/etc下运行时用复制后的)vim redis.conf

    修改如下配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 允许访问的地址,默认是127.0.0.1,会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问,生产环境不要设置为0.0.0.0
    bind 0.0.0.0
    # 守护进程,修改为yes后即可后台运行logs
    daemonize yes
    # 密码,设置后访问Redis必须输入密码
    requirepass 123321
    # 监听的端口
    port 6379
    # 工作目录,默认是当前目录,也就是运行redis-server时的命令,日志.持久化等文件会保存在这个目录
    dir .
    # 数据库数量,设置为1,代表只使用1个库,默认有16个库,编号0~15
    databases 1
    # 设置redis能够使用的最大内存
    maxmemory 512mb
    # 日志文件,默认为空,不记录日志,可以指定日志文件名
    logfile "redis.log"
  10. 执行redis-server redis.conf 进行后台运行(redis-server是直接前台运行)

  11. 设置开机自启动

    1. 新建一个系统服务文件:vi/etc/systemd/system/redis.service

    2. 写入内容

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      [Unit]
      Description=redis-server
      After=network.target

      [Service]
      Type=forking
      ExecStart=/usr/local/bin/redis-server /opt/redis-6.2.1/redis.conf
      PrivateTmp=true

      [Install]
      WantedBy=multi-user.target
    3. 重载系统服务:systemctl daemon-reload

    4. 可以使用的命令:

      1
      2
      3
      4
      5
      6
      7
      8
      # 启动
      systemctl start redis
      # 停止
      systemctl stop redis
      # 重启
      systemctl restart redis
      # 查看状态
      systemctl status redis
    5. 让redis开机自启动:systemctl enable redis

连接redis

使用redis-cli

Redis安装完成后就自带了命令行客户端:redis-cli,使用方式如下:

1
redis-cli [options] [commonds]

其中常见的options有:

  • -h 127.0.0.1:指定要连接的redis节点的IP地址,默认是127.0.0.1
  • -p 6379:指定要连接的redis节点的端口,默认是6379
  • -a 123321:指定redis的访问密码

其中的commonds就是Redis的操作命令,例如:

  • ping:与redis服务端做心跳测试,服务端正常会返回pong
使用图形化界面

这个仓库可以找到安装包:https://github.com/lework/RedisDesktopManager-Windows/releases

下载后一直next即可

Key键的通用操作

  • keys *查看当前库所有key (匹配:keys *1,不建议在生产环境设备上使用)
  • exists key判断某个key是否存在
  • type key 查看你的key是什么类型
  • del key 删除指定的key数据
  • unlink key 根据value选择非阻塞删除(仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。)
  • expire key 10 10秒钟:为给定的key设置过期时间
  • ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
  • select命令切换数据库
  • dbsize查看当前数据库的key的数量
  • flushdb清空当前库
  • flushall通杀全部库

key的结构

很多时候为了避免key重复,可以将key设计成 项目名:业务名:类型:id的形式

如:

KEY VALUE
project:user:1 {“id” :1,”name” : “Jack” ,”age”: 21}
project:product:1 { “id” :1,”name” :”小米11”,”price”: 4999}

Redis数据库键空间

Redis是一个键值对( key-value pair)数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间( key space )

为了实现从键到值的快速访问,Redis 键空间使用了一个哈希表来保存所有键值对(称为全局哈希表):

image-20230414162642297

哈希冲突问题:

当你往哈希表中写入更多数据时,哈希冲突是不可避免的问题。这里的哈希冲突,也就是指,两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。

Redis为了解决hash冲突问题,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

Redis 开始执行 rehash看,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2,这个过程分为三步:

  1. 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
  2. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
  3. 释放哈希表 1 的空间。

渐进式 rehash

由于如果在拷贝步骤涉及大量数据,会造成Redis线程阻塞。

简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。

这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

常用数据类型

String字符串

常见命令

  • set 添加键值对

  • get 查询对应键值

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

  • strlen 获得值的长度

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

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

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

  • incrby / decrby <步长>:将 key 中储存的数字值增减。自定义步长。

  • incrbyfloat/decrbyfloat 步长:将 key 中储存的浮点数增减。自定义步长。

  • mset ….. 同时设置一个或多个 key-value对

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

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

  • getrange <起始位置><结束位置>

    获得值的范围,类似java中的substring,前包,后包

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

  • setex <**过期时间**>设置键值的同时,设置过期时间,单位秒。

  • getset 以新换旧,设置了新值同时获得旧值。

底层结构

String的数据结构为**简单动态字符串(Simple Dynamic String,缩写SDS)**。

简单动态字符串( simple dynamic string,SDS):

  • SDS的定义:

    1
    2
    3
    4
    5
    6
    7
    8
    struct sdshdr {
    //记录buf数组中已使用字节的数量等于sDs所保存字符串的长度
    int len;
    //记录buf数组中未使用字节的数量
    int free;
    //字节数组,用于保存字符串
    char buf[];
    };
  • 特点:

    • 常数复杂度获取字符串长度
    • 杜绝缓冲区溢出,能够进行动态扩展
    • 空间预分配((当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。)、惰性空间释放
    • 二进制安全,因为使用字节数组保存数据

List列表

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

常见命令

  • lpush/rpush …. 从左边/右边插入一个或多个值。

  • lpop/rpop 从左边/右边吐出一个值。值在键在,值光键亡。

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

  • lrange 按照索引下标获得元素(从左到右)

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

  • lindex 按照索引下标获得元素(从左到右)

  • llen 获得列表长度

  • linsert before 的后面插入插入值

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

  • lset将列表key下标为index的值替换成value

底层结构

其底层有ZipList和QuickList两种存储方式。

  • 首先在列表元素较少的情况下会使用一块连续的内存存储,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,这个结构是ZipList,也即是压缩列表。
  • 当数据量比较多的时候才会改成QuickList双向链表。既满足了快速的插入删除性能,又不会出现太大的空间冗余。

ZipList压缩列表:

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型( sequential )数据结构。一个压缩列表可以包含任意多个节点( entry ),每个节点可以保存一个字节数组或者一个整数值。

  • 结构:

    image-20230415163358752

    • zlbytes:表示列表总字节数

    • zltail:列表尾的偏移量

    • zllen:列表中的 entry 个数

    • entry:

      • previous_entry_length:记录了压缩列表中前一个节点的长度

      • encoding:记录了节点的content属性所保存数据的类型以及长度

      • content:数据内容

        image-20230415163712815
    • zlend:表示列表结束

  • 特点:

    • 要查找定位第一个元素和最后一个元素O(1),查找其他元素O(N)。
    • 节约空间。
    • 有可能会因为entry内容长度的问题,引发一系列的连锁更新,但很少发生。

QuickList双向链表

QuickList数据结构是Redis在3.2版本引入的一种新得数据结构,他是一个双端链表,链表中的每个节点都是一个 ZipList

  • 结构:

    image-20230415165300844

    • node结构:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      typedf struct quicklistNode{
      //前一个节点
      quicklistNode* prev;
      //后一个节点
      quicklistNode* next;
      //压缩列表
      ziplist* zl;
      //ziplist大小
      int32 size;
      //ziplist 中元素数量
      int16 count;
      //编码形式 存储 ziplist 还是进行LZF算法压缩储存的zipList
      int2 encoding;
      ...
      }quickListNode
    • 整个链表:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      typedf struct quicklist{
      //指向头结点
      quicklistNode* head;
      //指向尾节点
      quicklistNode* tail;
      //元素总数
      long count;
      //quicklistNode节点的个数
      int nodes;
      //压缩算法深度
      int compressDepth;
      ...
      }quickList
  • 特点:

    • 比传统链表更省空间,所以能存储大量数据。
    • 还能对数据进行压缩

Set集合

Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的**复杂度都是O(1)**。并且可以自动的去重。

常见命令

  • sadd ….. 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略

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

  • sismember 判断集合是否为含有该值,有1,没有0

  • scard返回该集合的元素个数。

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

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

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

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

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

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

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

底层结构

Set数据结构是dict字典(用哈希表实现的)和整数数组;

  • 当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
  • 其余情况就使用字典。

整数数组:

整数集合( intset)是集合键的底层实现之一。

  • 结构:

    image-20230415170258415
    1
    2
    3
    4
    5
    6
    7
    8
    typedef struct intset {
    //编码方式
    uint32_t encoding;
    //集合包含的元素数量
    uint32_t length;
    //保存元素的数组,里面按照从小到大的顺序排列,不会有重复项
    int8_t contents[];
    }intset;
  • 特点:

    • 只存整数,保证了数据不重复。
    • 并且会根据数据类型,进行升级降级。

字典:

字典,又称为符号表( symbol table)、关联数组( associative array)或映射(map),是一种用于保存键值对( key-value pair )的抽象数据结构。

  • 结构:

    image-20230414204548933

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 哈希表
    typedef struct dictht {
    //哈希表数组
    dictEntry**table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值,总是等于size-1
    unsigned long sizemask;
    //该哈希表已有节点的数量
    unsigned long used;
    } dictht;
    // 一个键值对Entry
    typedef struct dictEntry {
    //键
    void *key;
    //值
    union {
    void *val;
    uint64_tu64;
    int64_ts64;
    } v;
    //指向下个哈希表节点,形成链表
    struct dictEntry *next;
    } dictEntry;
  • 特点:

    • 快速查找O(1)的复杂度。
    • 不支持范围查找,只能全部遍历很慢。

Hash哈希

Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。类似Java里面的Map<String,Object>

常见命令

  • hset 集合中的 键赋值
  • hget 集合取出 value
  • hmset … 批量设置hash的值
  • hexists查看哈希表 key 中,给定域 field 是否存在。
  • hgetall :列出该hash集合的所有field和value
  • hkeys 列出该hash集合的所有field
  • hvals 列出该hash集合的所有value
  • hincrby 为哈希表 key 中的域 field 的值加上增量 1 -1
  • hsetnx 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .

底层结构

Hash类型对应的数据结构是两种:Ziplist(压缩列表),Hashtable(哈希表)。当field-value长度较短且个数较少时,使用Ziplist,否则使用Hashtable。

Zset集合

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。

不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。

常见命令

  • zadd …将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

  • zrange [WITHSCORES]返回有序集 key 中,下标在之间的元素带WITHSCORES,可以让分数一起和值返回到结果集。

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

  • zrevrangebyscore key maxmin [withscores] [limit offset count] 同上,改为从大到小排列。

  • zincrby 为元素的score加上增量

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

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

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

底层结构

zset底层使用了两个数据结构:

  • 如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员( member)是比较长的字符串时,Redis就会使用跳跃表+哈希表来作为有序集合键的底层实现,这样能保证各方面的性能。

    1
    2
    3
    4
    typedef struct zset { 
    dict *dict; // 字典,保存了从成员到分值的映射关系;
    zskiplist *zsl; // 跳跃表,按分值由小到大保存所有集合元素;
    } zset;
    • 跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
    • 哈希表的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
  • 其余使用ziplist压缩列表:每个 value/score 元素紧凑排列存在entry中,节省内存(会根据score进行排序,类似于数组排序:先遍历找位置,再后移节点腾出空间,所以只适用于小规模场景)

跳跃表:

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

  • 结构:

    image-20230415171501532

    • header:指向跳跃表的表头节点。
    • tail:指向跳跃表的表尾节点。
    • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
    • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。

    zskiplistNode跳跃表的节点:

    • 层指针(level):每个层都带有两个属性:前进指针和跨度
      • 前进指针用于访问位于表尾方向的其他节点。
      • 跨度则记录了前进指针所指向节点和当前节点的距离。
    • 后退指针(backward):节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
    • 分值(score):在跳跃表中,节点按各自所保存的分值从小到大排列。
    • 成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象。
  • 特点:

    • 由于维护了多级索引,查找时间平均为O(logN)。
    • 可以通过顺序性操作来批量处理节点。

跳跃表的排序是通过多层链表结构和随机跳跃算法来实现的。它不需要像平衡树那样保持完美平衡,因此在插入、删除和查找元素时可以获得更高的性能。(当插入一个新节点时,Redis 会从最高层链表开始,根据一个随机算法,以一定概率向下跳跃到下一层链表继续查找。这个随机算法的概率分布是一个指数分布,即跳跃到下一层链表的概率会随着层数的增加而逐渐减小。这样就可以在保证数据有序的前提下,尽可能地减少查找、插入和删除元素的时间复杂度。)

Redis的发布和订阅

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。并且可以根据频道来划分(支持任意数量的频道)

实现

  1. 客户端订阅频道进行监听

    SUBSCRIBE channel1

  2. 发布者发布消息到指定频道

    publish channel1 hello

  3. 客户端自动收到发布的消息

Redis6新数据类型

Bitmaps

(1) Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。

(2) Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

常见命令

  • setbit设置Bitmaps中某个偏移量的值(0或1)*offset:偏移量从0开始

  • getbit获取Bitmaps中某个偏移量的值

  • bitcount[start end] 统计字符串从start字节到end字节比特值为1的数量

  • bitop and(or/not/xor) [key…] bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。

HyperLogLog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

常见命令

  • pfadd < element> [element …] 添加指定元素到 HyperLogLog 中

  • pfcount [key …] 计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可

  • pfmerge [sourcekey …] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得

Geospatia

Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

常见命令

  • geoadd< longitude> [longitude latitude member…] 添加地理位置(经度,纬度,名称)
  • geopos [member…] 获得指定地区的坐标值
  • geodist [m|km|ft|mi ] 获取两个位置之间的直线距离
  • georadius< longitude>radius m|km|ft|mi 以给定的经纬度为中心,找出某一半径内的元素

更多命令见Redis命令参考

Jedis

连接步骤

  1. 引入依赖:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
    </dependency>
  2. 注意需要将redis.conf中的protected-mode配置调成no(设置了密码不用)、以及bind ip的配置给注释掉

  3. 并且关闭linux防火墙:systemctl stop firewalld.service

  4. 创建jedis客户端:

    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args) {
    //建立连接
    Jedis jedis = new Jedis("192.168.227.130", 6379);
    //设置密码
    jedis.auth("123456");
    //选择库
    jedis.select(0);
    }
  5. 进行操作如:jedis.set(key,value)跟redis命令一致

相关基本操作

大多与redis原本操作一致

  • Key通用的操作

    • 获取获取key值:jedis.keys(“*”)
    • 判断是否存在该key:jedis.exists(“k1”)
    • 判断超时时间:jedis.ttl(“k1”)
    • 获取key的值:jedis.get(“k1”)
  • String类型的操作

    • 设置值:jedis.set(“k1”, “v1”)
    • 多次设置值:jedis.mset(“str1”,”v1”,”str2”,”v2”,”str3”,”v3”)
    • 获取多个值:jedis.mget(“str1”,”str2”,”str3”)
  • List类型的操作

    • 插入jedis.lpush(“k1”,”22”,”333”);
    • 获取所有值:jedis.lrange(“mylist”,0,-1);
  • Set类型的操作

    • 插入值:jedis.sadd(“orders”, “order01”);
    • 获取值:jedis.smembers(“orders”);
    • 删除值:jedis.srem(“orders”, “order02”);
  • Hash类型的操作

    • 插入值:jedis.hset(“hash1”,”userName”,”lisi”);
    • 获取值:jedis.hget(“hash1”,”userName”);
    • 批量插入:jedis.hmset(“hash2”,map);
    • 批量获取:jedis.hmget(“hash2”, “telphone”,”email”);
  • Zset类型的操作

    • 插入值:jedis.zadd(“zset01”, 100d, “z3”);
    • 获取值:jedis.zrange(“zset01”, 0, -1);

Jedis连接池

Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此使用Jedis连接池代替Jedis的直连方式是比较理想的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.wht;

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

public class JedisConnectionFacotry {
private static final JedisPool jedisPool;

static {
//配置连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(8);
poolConfig.setMaxIdle(8);
poolConfig.setMinIdle(0);
poolConfig.setMaxWaitMillis(1000);
//创建连接池对象
jedisPool = new JedisPool(poolConfig,
"192.168.227.130",6379,1000,"1313");
}

public static Jedis getJedis(){
return jedisPool.getResource();
}
}

Springboot集成redis

SpringBoot是使用的SpringDataRedis来整合的redis(SpringData是Spring中数据操作的模块,包含对各种数据库的集成)

  • 提供了RedisTemplate工具类,其中封装了各种对Redis的操作
  • 提供了对不同Redis客户端的整合(Lettuce和Jedis)
  • 提供了RedisTemplate统一API来操作Redis
  • 支持Redis的发布订阅模型
  • 支持Redis哨兵和Redis集群
  • 支持基于Lettuce的响应式编程
  • 支持基于JDK.JSON.字符串.Spring对象的数据序列化及反序列化
  • 支持基于Redis的JDKCollection实现

基本使用

  • 导入依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- redis -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- spring2.X集成redis所需common-pool2-->
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.0</version>
    </dependency>
  • 在配置文件中配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    spring:
    redis:
    database: 0 #Redis数据库索引(默认为0)
    host: 192.168.227.130 #Redis服务器地址
    lettuce:
    pool:
    max-active: 20 #连接池最大连接数(使用负值表示没有限制)
    max-idle: 5 #连接池中的最大空闲连接
    max-wait: -1 #最大阻塞等待时间(负数表示没限制)
    min-idle: 0 #连接池中的最小空闲连接
    port: 6379 #Redis服务器连接端口
    timeout: 1800000 #连接超时时间(毫秒)
    password: root123456 #验证密码
  • 自定义RedisTemplate(因为RedisTemplate可以接收任意Object作为值写入Redis,但是默认使用的是JDK序列化这样可读性很差)

    image-20220727145828091

    方式一:(注意一定要导入json依赖或者web的starter)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    @EnableCaching
    @Configuration
    public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
    // 创建RedisTemplate对象
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    // 设置连接工厂
    template.setConnectionFactory(connectionFactory);
    // 创建JSON序列化工具
    GenericJackson2JsonRedisSerializer jsonRedisSerializer =
    new GenericJackson2JsonRedisSerializer();
    // 设置Key的序列化
    template.setKeySerializer(RedisSerializer.string());
    template.setHashKeySerializer(RedisSerializer.string());
    // 设置Value的序列化
    template.setValueSerializer(jsonRedisSerializer);
    template.setHashValueSerializer(jsonRedisSerializer);
    // 返回
    return template;
    }

    /**
    * JSON 序列化器
    */
    @Bean
    public RedisSerializer<Object> RedisSerializer() {
    Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
    ObjectMapper objectMapper = new ObjectMapper();
    // JavaTimeModule用于处理LocalDate类型
    objectMapper.registerModule(new JavaTimeModule());
    // ALL表示序列化对象中的所有成员(包括getter、setter、field等);ANY表示序列化所有可见度的成员(包括private)
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    // LaissezFaireSubTypeValidator.instance表示:不做验证,允许序列化所有实例
    // ObjectMapper.DefaultTyping.NON_FINAL表示:除了final声明的值和基本类型外,都会在序列化时添加类名标识(final不应被序列化)
    objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
    serializer.setObjectMapper(objectMapper);
    return serializer;
    }
    }
  • 使用redisTemplate

    redisTemplate的操作对象

    image-20220726223442480

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RestController
    @RequestMapping("/redisTest")
    public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping
    public String testRedis() {
    //设置值到redis
    redisTemplate.opsForValue().set("name","lucy");
    //从redis获取值
    String name = (String)redisTemplate.opsForValue().get("name");
    return name;
    }
    }
  • 使用StringRedisTemplate

    为了节省内存空间,我们可以不使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。这样必须由我们自己手动的来序列化、反序列化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    void test03(){
    User user = new User(1L, "jack");
    //使用stringRedisTemplate存入自己转换的json
    stringRedisTemplate.opsForValue().set("user:3",JSON.toJSONString(user));
    //转换获取出来的json字符串
    User user1 = JSON.parseObject(stringRedisTemplate.opsForValue().get("user:3"),User.class);
    System.out.println(user1);
    }

    更多RedisTemplate的方法参考其他博客

Springboot 的缓存抽象

Spring从3.1开始定义了org.springframework.cache.Cache,Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache (JSR-107)注解简化我们开发。

每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

使用Spring缓存抽象时我们需要关注以下两点:

  • 确定方法需要被缓存以及他们的缓存策略
  • 从缓存中读取之前缓存存储的数据

重要概念

  • Cache:缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
  • CacheManager:缓存管理器,管理各种缓存(Cache)组件
  • keyGenerator:缓存数据时key生成策略
  • serialize:缓存数据时value序列化策略

常用注解

  • @EnableCaching:开启基于注解的缓存

  • @Cacheable:主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

    • 属性:
    • cacheNames/value:指正缓存组件的名字
    • key:缓存数据使用的key,可以用它来指定。默认是使用方法参数的值(编写SpEL; #id;参数id的值#a#p日#root.args[0])
    • keyGenerator: key的生成器;可以自己指定key的生成器的组件id(key/keyGenerator:二选一使用)
    • cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器
    • condition:指定符合条件的情况下才缓存
    • unless:否定缓存:当unless指定的条件为true,方法的返回值就不会被缓存
      • 可以获取到结果进行判断unless ="#result == nuLL"
    • sync:是否使用异步模式
  • @CachePut:保证方法被调用,又希望结果被缓存。(常用于更新方法)

  • @CacheEvict:方法被调用后清空缓存。(常用于删除方法)

    • 可以配置allEntries = true清空所有缓存
    • beforeInvocation = false:缓存的清除是否在方法之前执行
      默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除
    • beforeInvocation = true:
      代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除

使用Redis当缓存

springboot默认使用ConcurrentMapCacheManager以及ConcurrentMapCache

  1. 自定义keyGenerator

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * 配置默认键生成类
    */
    @Override
    public KeyGenerator keyGenerator() {
    return myKeyGenerator();
    }

    /**
    * 自定义键生成类 [类名_方法名_参数]
    */
    KeyGenerator myKeyGenerator(){
    return (target, method, params) -> target.getClass().getSimpleName() + "_"
    + method.getName() + "_"
    + StringUtils.arrayToDelimitedString(params, "_");
    }
  2. 自定义 CacheManager(由于默认缓存管理器还是用jdk序列化的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 缓存管理器
    */
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
    RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())) // 设置键序列化方式
    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer)) // 设置值序列化方式
    .entryTtl(Duration.ofHours(1)) // 设置缓存有效期
    .disableCachingNullValues(); // 不缓存空值
    return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }

    完整配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.StringUtils;
import java.time.Duration;

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

@Autowired
RedisSerializer<Object> jsonSerializer;

/**
* 自定义RedisTemplate
* @param connectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
// 设置Key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 设置Value的序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
// 返回
return template;
}

/**
* JSON 序列化器
*/
@Bean
public RedisSerializer<Object> RedisSerializer() {
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// JavaTimeModule用于处理LocalDate类型
objectMapper.registerModule(new JavaTimeModule());
// ALL表示序列化对象中的所有成员(包括getter、setter、field等);ANY表示序列化所有可见度的成员(包括private)
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// LaissezFaireSubTypeValidator.instance表示:不做验证,允许序列化所有实例
// ObjectMapper.DefaultTyping.NON_FINAL表示:除了final声明的值和基本类型外,都会在序列化时添加类名标识(final不应被序列化)
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
return serializer;
}


/**
* 配置默认键生成类
*/
@Override
public KeyGenerator keyGenerator() {
return myKeyGenerator();
}

/**
* 自定义键生成类 [类名_方法名_参数]
*/
KeyGenerator myKeyGenerator(){
return (target, method, params) -> target.getClass().getSimpleName() + "_"
+ method.getName() + "_"
+ StringUtils.arrayToDelimitedString(params, "_");
}

/**
* 缓存管理器
*/
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())) // 设置键序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer)) // 设置值序列化方式
.entryTtl(Duration.ofHours(1)) // 设置缓存有效期
.disableCachingNullValues(); // 不缓存空值
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
}

封装工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
package com.wht.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================

/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.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 redisTemplate.getExpire(key, TimeUnit.SECONDS);
}

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

/**
* 删除属性
*/
public Boolean del(String key) {
return redisTemplate.delete(key);
}

/**
* 批量删除属性
*/
public Long del(List<String> keys) {
return redisTemplate.delete(keys);
}
// ============================String=============================

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

/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.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, Object value, long time) {
try {
if (time > 0) {
redisTemplate.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 redisTemplate.opsForValue().increment(key, delta);
}

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

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

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

/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.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, Object> map, long time) {
try {
redisTemplate.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 {
redisTemplate.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 {
redisTemplate.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) {
redisTemplate.opsForHash().delete(key, item);
}

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

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

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

/**
* 根据key获取Set中的所有值
*
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.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 redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.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, Object... values) {
try {
Long count = redisTemplate.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 redisTemplate.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 = redisTemplate.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<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.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 redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.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, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

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

/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.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, Object value) {
try {
redisTemplate.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 {
Long remove = redisTemplate.opsForList().remove(key, count,value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}

事务和锁机制

Redis事务特性:

  • 单独的隔离操作 :事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • 没有隔离级别的概念 :队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

  • 不保证原子性 :事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

事务相关命令

image-20220526223634699

  • Multi:从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec
  • Exec:Redis会将之前的命令队列中的命令依次执行
  • discard:组队的过程中可以通过discard来放弃组队。

事务错误机制

  1. 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。

image-20220526225010816

  1. 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

image-20220526225025802

锁机制

悲观锁

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

乐观锁

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

相关命令

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

  • unwatch :取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

Redis持久化

因为Redis的数据是放在内存中的,持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 提供了2个不同形式的持久化方式:

  • RDB(Redis DataBase)
  • AOF(Append Of File)

RDB

在指定的时间间隔内将内存中的数据集快照写入磁盘(具体操作:Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件)

使用

默认情况下,Redis 将数据集的快照保存在磁盘上的一个名为dump.rdb.

如果数据集中至少有 M 次更改,您可以将 Redis 配置为每 N 秒保存一次数据集,或者您可以手动调用SAVEorBGSAVE命令

如:save 60 1000:如果至少更改了 1000 个键,则此配置将使 Redis 每 60 秒自动将数据集转储到磁盘

如:bgsave :命令用于在后台异步保存当前数据库的数据到磁盘

使用场景:

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高更适合使用
  • 节省磁盘空间
  • 恢复速度快

缺点

  • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  • 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  • 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

AOF

日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量),redis 重启的时候就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

使用

因为AOF默认不开启,需要在redis.conf中配置

设置appendonly yes

从现在开始,每次 Redis 接收到更改数据集的命令(例如SET时,它都会将其附加到 AOF。当您重新启动 Redis 时,它将重新播放 AOF 以重建状态。

AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失),所以建议两个都启用,不建议单独使用AOF

AOF的同步频率
  • appendfsync always:始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
  • appendfsync everysec:每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
  • appendfsync no:redis不主动进行同步,把同步时机交给操作系统。

缺点:

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

  • 恢复备份速度要慢。

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

  • 存在个别Bug,造成恢复不能。

IO多线程

Redis6终于支撑多线程了,告别单线程了吗?

IO多线程其实指客户端交互部分网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程。

开启多线程

  • 进入配置文件

  • 修改配置:

    1
    2
    3
    4
    #是否开启多线程
    io-threads-do-reads yes
    #线程数
    io-threads 4