Redis深入了解

redis的八大类型
基本数据类型:

  • String
  • List
  • Set
  • Hash
  • Zset

三种特殊数据类型:

  • geo
  • hyperloglog
  • bitmap

学习Redis之前我们首先要先了解一下NoSql

NoSql了解

单机mysql

在这里插入图片描述

90年代 , 互联网发展不是很普及 , 数据量也小

单机mysql时代的问题:

  1. 数据量太大 , 一个机器放不下了
  2. 数据的索引 , mysql数据库单表超过300万条数据 , 一定要建立索引(B+ tree) 如果数据量太大了 , 索引在一个机器内存中也放不下
  3. 数据库访问量(读写混合) , 一台服务器承受不了

Memcached(缓冲) + Mysql + 垂直拆分

发展历史: 优化数据结构和索引 ----> 文件缓冲(IO) —> Memcached(读写分离)
在这里插入图片描述

分库分表(水平拆分) MySQL集群

本质:数据库(读写)

早些年 MyISAM: 表锁 ,十分影响效率 ! 高并发下机会出现严重的锁问题

早些年 Innodb: 行锁

慢慢使用分库分表解决写的压力! MySQL在那个年代推出了表分区 , 没有多少人使用 , 后来推出了集群

在这里插入图片描述

当今

MySQL等关系型数据库不够用了 , 数据量大 , 变化快

大数据IO压力下 , 表几乎无法更改

目前基本的互联网项目

在这里插入图片描述

为什么要用NoSQL

用户的个人信息 , 社交网络 , 地理位置 , 用户自己产生的数据 , 用户的日志等等爆发式增长!

这时我们就需要使用NoSQL数据库了

什么是NoSQL = Not Only Sql (不仅仅是SQL)

泛指非关系型数据库 , 随着web2.0互联网的诞生! 传统的关系型数据库很难对付web2.0时代 , 尤其是超大规模的高并发

关系型数据库 , 行和列记录值 , NoSQL在当今大数据环境下发展特别快

NoSQL的特点

  1. 方便扩展(数据之间没有关系 , 很好扩展)
  2. 大数据量高性能(redis 一秒 写 8万次 , 读 11万次 , NoSQL的缓冲 是一种细粒度的缓冲 , 性能会比较高)
  3. 数据类型是多类型的!(不需要事先设计数据库 , 随取随用)

传统的关系型数据库(RDBMS)

  • 结构化组织
  • SQL
  • 数据和关系都存在单独的表中
  • 操作语言 , 定义语言
  • 严格的一致性
  • 基础的事物

NoSQL

  • 不仅仅是数据
  • 没有固定的查询语言
  • 简直对存储 , 列存储 , 文档存储 , 图形数据库
  • 最终一致性
  • CAP定理 , 和BASE理论 (异地多活)
  • 高性能 , 高可用 , 高可扩

大数据时代的3V: 主要是描述问题的

  • 海量(Volume)
  • 多样(Variety)
  • 实时(Velocity)

大数据时代的3高: 主要是对程序的要求

  • 高并发
  • 高可扩
  • 高性能

NoSQL四大分类

KV键值对:

  • 新浪:Redis
  • 美团: Redis + Tair
  • 阿里百度:Redis + Memecache

文档型数据库:(Bson格式和json一样)

  • MongDB

    • MongDB是一个基于分布式文件存储的数据库 , C++编写 , 主要用来处理大量的文档
    • MongDB是一个介于关系型数据库和非关系型数据库之间的产品 , MongDB是非关系型数据库中功能最丰富,最像关系型数据库的
  • ConthDB

列存储数据库:

  • HBase
  • 分布式文件系统

图形关系数据库:
不是存图形的 , 是存的关系

  • Neo4j 、 InfoGrid

Redis学习

redis是什么

Redis(Remote Dictionary Server ),即远程字典服务

是一个开源的使用ANSI C语言 编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的API。

Redis能干嘛?

  1. 内存存储、持久化 , 内存中是断电即失的 , 所以说持久化很重要(rdb 、aof)
  2. 效率高可用于高速缓冲
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器、计数器(浏览量)

特性

  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务

redis安装

  1. windows安装

    1. 下载安装包 https://github.com/MicrosoftArchive/redis/releases

    2. 下载完毕得到压缩包 , 解压压缩包

    3. 开启redis , 默认端口是 6379 redis-cli.exe (客户端) redis-server.exe (服务端)

    4. 使用redis客户端链接redis

      ping    #  测试链接
      set name yyf    # 设置kv键值对 , 键:name   值: yyf
      get name     # 获取键为name的值
      
  2. Linux安装

    1. 下载安装包 http://www.redis.cn/

    2. 上传redis压缩包 , 移动倒opt目录

      mv redis-6.0.6.tar.gz /opt
      
    3. 切换路径到opt下面

      cd /opt
      
    4. 解压Redis安装包

      tar -zxvf redis-6.0.6.tar.gz
      
    5. 进入解压后的文件 , 可以看到redis的配置文件 , redis.conf

      cd ./redis-6.0.6
      
    6. 基本的环境安装

      yum install gcc-c++   # 安装gcc环境
      gcc -v  # 查看gcc的环境版本
      make # 安装所有内容
      make # 再次安装所有内容
      make install # 安装redis
      
    7. make时可能会报一大堆错 , 这种情况下需要执行一系列命令

      # 安装gcc套装:
      yum install cpp
      yum install binutils
      yum install glibc
      yum install glibc-kernheaders
      yum install glibc-common
      yum install glibc-devel
      yum install gcc
      yum install make
      
      # 升级gcc
      yum -y install centos-release-scl
      
      yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
      
      scl enable devtoolset-9 bash
      
      # 设置永久升级:
      echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile
      
      # 开始安装redis , 按照上述步骤执行make以及后续命令
      
      make
      
    8. redis的默认安装路径 /usr/local/bin

    9. 拷贝redis下面的配置文件 redis.conf

      mkdir kconfig
      cp /opt/redis-6.0.6/redis.conf kconfig/
      
    10. 我们之后就使用拷贝出来的配置文件去使用 , 这样我们可以在不改变原来配置文件的前提下做 , 因此我们即使改坏配置文件也可以继续改回来

    11. 修改配置文件 , 使redis可以后台启动

      vim redis.conf
      
      # 将配置文件中的daemonize  的值改为  yes
      
    12. 启动redis服务

      redis-server kconfig/redis.conf
      
    13. 启动redis客户端进行测试

      redis-cli -p 6379
      ping # 测试连通
      
      set name yyf   # 存值
      get name       # 取值
      
      keys *   # 查看所有的key
      
      
    14. 查看redis进程是否开启

      ps -ef | grep redis
      
    15. 关闭redis服务

      shutdown
      
    16. redis-benchmark 是一个压力测试工具 , 官方自带的性能测试工具

      # 测试100 个多并发连接 , 100000个请求
      
      redis-benchmark -h localhost -p 6379 -c 100 -n 100000
      
    17. 如何查看这些分析

      在这里插入图片描述

redis基础知识

默认使用的是第0个数据库

select 3   # 使用3号数据库

dbsize    # 查看当前数据库大小

flushdb   # 清空当前库

flushall  # 清空全部数据库

Redis是单线程的!!!

Redis是很快的 , 官方表示 , Redis是基于内存操作 , CPU不是Redis性能瓶颈 , Redis的瓶颈是根据机器的内存和网络带宽 , 这样单线程就可以做的事情 , 所以就使用单线程了

Redis所C语言写的 . 官方提供 的数据为 100000+的QPS , 完全不比同样使用kv键值对的Memecache差!

Redis 为什么单线程还这么快?

  • 核心: Redis是将所有的数据全部放在内存中的 , 所以说使用单线程效率就是最高的
  • 多线程的话 , CPU会上下文切换 , 是一个很耗时的操作
  • 对于内存系统 , 如果没有上下文切换 , 效率就是最高的
  • 多次读写都在一个CPU上的 , 内存情况下 , 单线程几十号最佳的方案

五大基本数据类型

  1. String类型(字符串)
  • 判断是否存在

    EXISTS age   # 判断名为age的键是否存在 , 存在返回1 , 不存在返回0
    
  • 移除

    move age 1   # 移除1号数据库的age这个键
    
  • 设置过期时间

    EXPIRE age 10  # 设置age这个键10秒过期
    
  • 查看过期时间

    ttl age  # 查看age还有几秒过期
    
  • 查看类型

    type name # 查看当前键为name的类型
    
  • 追加字符串

    APPEND name "a"   # 向键为name的值中追加字符串字符串a , 如果当前key不存在就相当于setKey
    
  • 获取字符串长度

STRLEN name  # 获取键为name的值得字符串长度
  • 加减运算

    INCR views   # 给views这个键的值 + 1
    
    DECR views   # 给当前浏览量 - 1
    
    INCRBY views 10   # 给键为views的值 + 9
    
    DECRBY views  5 # 给当前浏览量 - 5
    
  • 截取字符串

    GETRANGE name 3 5    # 截取字符串  从下标[3,5]
    
    GETRANGE name 0 -1   # 截取字符串  到-1即是查看字符串全部
    
  • 替换指定字符串

    set name "abcdefg"   # 设置name的值为abcdefg
    SETRANGE name 1 xx    #  将字符串从1号下标的位置开始替换 , 替换两个字符为xx , 即 将abcdefg替换为 axxdefg
    
  • 组合命令创建键值对

    setex(set with expire)   # 设置过期时间
    setnx (set if not exist)  # 如果不存在设置 (在分布式锁中常常使用)
    
    SETEX key3 30 "hello"  # 设置key3的值为hello , 并且让key3在30秒后过期
    SETNX mykey "redis"   # 判断mykey不存在 则 创建mykey
    SETNX mykey "mongDB"  # 判断mykey存在 则 创建失败
    
  • 批量创建和获取值

    mget
    mset
    
    mset k1 v1 k2 v2 k3 v3  # 批量设置键值对
    mget k1 k2 k3   # 批量查看键的值
    MSETNX k1 v1 k4 v4   # 判断不存在 则 创建 , 这里k1已经存在 , 所以设置k4即便不存在也设置失败了 , 就是要么一起成功, 要么一起失败
    
  • 对象

    set user:1{name: zhangsan , age: 3}  # 设置一个user:1 对象 值为json字符来保存一个对象
    
    # 这里的key是一个巧妙地设计 , user:{id}:{filed}  这样的设计在redis中是完全OK的
    
    mset user:1:age 30 user:1:sex "nan"  # 批量设置键值
    mget user:1:age user:1:sex   # 批量获取值
    
    
    
  • getset 组合命令

    getset  # 组合命令 先get再set 
    
    getset db redis  # 如果不存在值 , 则返回nil
    getset db mongodb   # 如果存在值 , 获取原来的值 , 再设置新的值
    
    

    String类型的使用场景 : value除了是我们的字符串 , 还可以是我们的数字

    • 计数器
    • 统计多单位的数量
    • 对象缓冲存储
    • 粉丝数
  1. List类型
    基本的数据类型 , 在redis中我们可以把list当做栈、队列、阻塞队列~
    与list相关的命令都是以L开头的

    1. list存值 lpush
    LPUSH list one   # 将一个值(one)或多个值插入到列表(list)的头部(左)
    
    RPUSH list one   # 将一个值(one)或多个值插入到列表(list)的尾部(右)
    
    1. 查看列表中的值 lrange
    LRANGE list 0 -1  # 查看列表中的所有值
    
    LRANGE list 0 1  # 查看列表中下标[0,1]的值
    
    1. 移除list lpop rpop
    LPOP list   # 从首部(左边)移除list中的一个元素
    
    RPOP list   # 从尾部(右边)边移除list中的一个元素
    
    1. 根据索引查看list的值 lindex
    LINDEX list 0   #  查看从左边数下标为0的位置的list
    
    1. 查看list的长度 llen
    LLEN list   #  查看list的长度
    
    1. 移除指定的值 lrem
    LREM list 1 threwe   # 移除 1 个值  ,  值为threwe
    
    # 如果键一样 , 可以同时移除
    LREM list 2 three      #  同时移除两个值为three的
    
    1. 修剪: 截断操作 , 只保留list一部分 ltrim
    LTRIM list 1 2    # 只要list中下标[1,2]的元素 , list被修改了 , 只剩下截取的元素
    
    1. 组合命令(rpoplpush) 移除列表的最后一个元素 , 并将他移动到新的列表中 rpoplpush
    RPOPLPUSH list mylist   #  移除列表(list)的尾部(最后)一个元素 , 并将他移动到新的列表(mylist)首部(左边)中
    
    
    127.0.0.1:6379> LRANGE list 0 -1
    1) "hello2"
    2) "hello1"
    127.0.0.1:6379> RPOPLPUSH list mylist   #  移除列表(list)的尾部(最后)一个元素 , 并将他移动到新的列表(mylist)首部(左边)中
    "hello1"
    127.0.0.1:6379> lrange list 0 -1        
    1) "hello2"
    127.0.0.1:6379> lrange mylist 0 -1
    1) "hello1"
    
    1. 判断list是否存在 exists
    EXISTS list  # 判断list是否存在
    
    1. 更新指定下标的值(将列表中指定下标的值替换为另外一个值) lset
    lset list 0 item
    
    
    127.0.0.1:6379> LSET list 0 item   # 如果列表不存在直接lset就会报以下错
    (error) ERR no such key 
    127.0.0.1:6379> LPUSH list value   # 创建列表list并向其中追加一个值value
    (integer) 1
    127.0.0.1:6379> LRANGE list 0 0    # 查看列表list的值
    1) "value"
    127.0.0.1:6379> lset list 0 item   # lset将list列表中的 0 号下标的元素替换为item
    OK
    127.0.0.1:6379> LRANGE list 0 0    # 查看列表list的值
    1) "item"
    127.0.0.1:6379> LSET list 1 other  # lset一个列表中不存在的下标 , 报错
    (error) ERR index out of range
    
    1. 向列表中插入一个值 linsert (将某个具体的value插入到列表中某个元素的前面或后面)
    LINSERT list before item other  # 向list中的item元素的左边(前边)插入一个值 other
    LINSERT list after item world   # 向list中的item元素的右边(后边)插入一个值 world
    
    
    127.0.0.1:6379> LRANGE list 0 -1   # 查看list的值
    1) "item"
    
    127.0.0.1:6379> LINSERT list before item other  # 向list中的item元素的左边(前边)插入一个值 other
    
    127.0.0.1:6379> LRANGE list  0 -1     # 查看list的值
    1) "other"
    2) "item"
    
    127.0.0.1:6379> LINSERT list after item world   # 向list中的item元素的右边(后边)插入一个值 world
    (integer) 3
    127.0.0.1:6379> lrange list 0 -1   # 查看list中的元素
    1) "other"
    2) "item"
    3) "world"
    127.0.0.1:6379> 
    
    • 他实际上是一个列表 , before Node after left和right都可以插入值
    • 如果key不存在 , 创建新的链表
    • 如果key存在 , 新增内容
    • 如果移除了所有的值 , 空链表 , 也代表不存在!
    • 在两边插入或改动值效率最高 , 中间元素 , 效率相对来说会低一点
  2. Set类型(集合)
    无序不重复集合
    set中的命令都是以s开头

    1. 创建set集合添加元素
    SADD myset "hello" "world"  # 创建set集合并向其中添加两个值
    
    1. 查看set集合中的值
    SMEMBERS myset    # 查看myset这个set集合中的 所有元素
    
    1. 判断set集合中某个元素是否存在
    SISMEMBER myset hello   # 判断myset集合中是否存在hello这个元素 , 存在返回1 ,  不存在返回0
    
    1. 获取set集合中的元素个数
    SCARD myset   # 查看myset这个set集合中的元素个数
    
    1. 移除set集合中的指定元素
    SREM myset hello    # 移除myset这个set集合中的hello这个元素
    
    1. 随机抽选一个元素
    SRANDMEMBER myset   # 随机抽选一个元素
    
    SRANDMEMBER myset 2  # 随机抽选两个元素
    
    1. 删除指定的key , 随机删除一个key
    SPOP myset  # 随机弹出一个元素
    
    SPOP myset 2  # 随机弹出指定个数个元素 ,  给myset这个set集合随机弹出2个元素
    
    1. 将一个指定的值移动到另外一个set集合中
    SMOVE myset myset2 hello   #  将myset中的元素hello 移动到 myset2中
    
    1. 求差集 , 交集 , 并集
    SDIFF key1 key2    #  差集 , 以key1为参照 ,  也就是看key1中有key2中没有的值
    
    SINTER key1 key2   #  交集 , 查找key1 与 key2中都有的元素
    
    SUNION key1 key2   #  并集 , 查找key1 与 key2 中所有的元素
    
    
  3. Hash类型(哈希)

  • map集合 , key-map , 这时这个值时个map集合

  • 本质和string类型没有太大区别 , 还是key-value , value是由key-value的

  • hash 的组成 , hash filed value

  • hash中存放的依旧是键值对 , hash中存放的键值对中键为filed 值为 value

    1. 设置hash值&获取hash值
    hSET myhash filed1 hello    # 创建myhash这样一个hash类型并设置指定值 , 这个值中的键是filed1 , 值是hello
    
    HGET myhash filed1    # 获取myhash中的值 , 这个值时键值对 , 所以获取这个键值对中键为filed1的值
    
    1. 批量设置hash值&获取多个hash值
    hMSET hash filed1 hello filed2 world   # 创建hash类型并批量设置值
    
    HMGET hash filed1 filed2      # 批量获取值
    
    HGETALL hash    # 获取全部的hash值 , 以key-value展示
    
    1. 删除一个hash值
    HDEL hash filed1  # 删除hash的指定的key字段! 对应的value值也就没有了
    
    1. 获取hash值得长度
    HLEN hash   # 获取hash中元素个数
    
    1. 判断hash中指定字段是否存在
    HEXISTS hash filed2   # 判断hash中的key是否存在
    
    1. 只获得hash中所有的key&只获得所有的值
    HKEYS hash    # 获取所有的filed
    
    HVALS hash    # 获取所有的value
    
    1. 自增 & 自减
    HINCRBY myhash filed1 1   # 设置myhash这个hash值中的键为filed的值自增1
    
    HINCRBY myhash filed1 -1   # 设置myhash这个hash值中的键为filed的值自减1
    
    1. 判断是否存在 , 存在创建失败 , 不存在则创建hash
    HSETNX hash filed6 world    # 判断hash中filed6是否存在 , field6不存在则创建
    HSETNX hash filed6 yyf      # 判断hash中filed6是否存在 , field6存在则创建失败
    
  1. Zset类型(有序集合)
    在set的基础上增加了一个值

    1. 设置有序集合的值(zset和set基本一样 , zset比set增加了一个标志位 , 记录顺序)
    ZADD zset 1 one   # 添加一个值
    
    zadd zset 3 three 4 fourth   # 添加多个值
    
    1. 遍历查询所有zet中的值
    ZRANGE zset 0 -1   # 查询zset中的所有值
    
    1. 排序
    ZRANGEBYSCORE salary -inf +inf   # 排序   给salary中的值按照标志位进行排序  ,  -inf是负无穷 , +inf 是正无穷 , 这里只输出值
    ZRANGEBYSCORE salary -inf +inf withscores   #  加上withscores , 就是把键值对都输出
    ZRANGEBYSCORE salary -inf 2500     #  这里是只查询从负无穷到2500的值
    ZREVRANGE salary 0 -1    #  按照降序排列
    
    
    # 例子:   添加三个用户
    127.0.0.1:6379> ZADD salary 2500 xiaoming
    (integer) 1
    127.0.0.1:6379> ZADD salary 5000 zhangsan
    (integer) 1
    127.0.0.1:6379> ZADD salary 500 yyf
    (integer) 1
    
    127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf    # 给salary中的值按照标志位进行排序 
    1) "yyf"
    2) "xiaoming"
    3) "zhangsan"
    
    127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores  # 把值和标志位都输出
    1) "yyf"
    2) "500"
    3) "xiaoming"
    4) "2500"
    5) "zhangsan"
    6) "5000"
    127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500   # 输出负无穷到2500区间的值
    1) "yyf"
    2) "xiaoming"
    
    127.0.0.1:6379> ZREVRANGE salary 0 -1   # 按照降序排列
    1) "zhangsan"
    2) "yyf"
    127.0.0.1:6379> 
    
    
    1. 移除元素
    127.0.0.1:6379> ZRANGE salary 0 -1  # 查看salary中的所有值
    1) "yyf"
    2) "xiaoming"
    3) "zhangsan" 
    127.0.0.1:6379> ZREM salary xiaoming    # 移除salary中值为xiaoming的这个元素
    (integer) 1
    127.0.0.1:6379> ZRANGE salary 0 -1  # 查看salary中的所有值
    1) "yyf"
    2) "zhangsan"
    127.0.0.1:6379>
    
    1. 查看元素个数
     ZCARD salary  # 获取有序集合中的个数
    
    1. 计算区间变量
    ZCOUNT set 0 1    #  获取   标志位从0到1有多少个值   [0,1]
    # 获取指定区间的成员数量
    
    
    127.0.0.1:6379> ZADD set 1 hello 2 name 3 world 4 yyf 5 xiaoming  # 添加值
    (integer) 5
    127.0.0.1:6379> ZRANGE set 0  -1   # 查询全部的值
    1) "hello"
    2) "name"
    3) "world"
    4) "yyf"
    5) "xiaoming"
    127.0.0.1:6379> ZCOUNT set 1 3   # 查询  标志位从一到三之间有多少个值
    (integer) 3
    127.0.0.1:6379> ZCOUNT set 2 3   # 查询  标志位从二到三之间有多少个值   
    (integer) 2
    

三种特殊数据类型

  1. geospatial:
    定位、附近的人,城市距离计算等位置相关操作
    可以查询一些测试数据:http://www.jsons.cn/lngcode
    redis的Geo
    在这里插入图片描述

底层是zset实现 , 所以我们可以通过zet的相关函数操作geospatial类型

# 添加尘世地理位置
# 规则:两级无法直接添加 , 一般下载城市数据直接通过java程序一次性导入
# 参数:key  值(维度 , 经度 , 城市值)
GEOADD china:city 108.96 34.26 xian 

# 获取指定地点的经纬度
GEOPOS china:city  beijing chongqing

# 计算距离
GEODIST china:city shanghai beijing km  # 求北京到上海之间的距离 单位km

# 通过半径查询附近的人
#以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
GEORADIUS china:city 110 30 1000 km  # 以1110 30 为圆心 , 1000km为半径的附近的元素

GEORADIUS china:city 110 30 1000 km withdist withcoord count 1  # 查询以110 30这个经纬度 为圆心, 1000 km  为半径的附近的元素 , count 1   只查询1个元素 , withdist  查询之间的直线距离 , withcoord  查询经纬度显示

# 找出位于指定元素周围的其他元素
GEORADIUSBYMEMBER china:city beijing 1000 km   # 以beijing这个元素的位置为中心查找1000 km以内的元素

# 返回一个或多个位置元素的 Geohash 表示。返回一个或多个位置元素的 Geohash 表示。

GEOHASH china:city beijing chongqing   #将二维的经纬度转换为一维的字符串 , 如果两个字符串越接近 , 距离越近
  1. hyperloglog
    基数:(不重复的元素集合中有多少个)
    优点:占用的内存很小
PFADD mykey a b c d e f g h i j   # 添加mykey

PFCOUNT mykey    #  查看mykey中的不重复的数据元素个数

PFADD mykey2  d d d v f g q g h z n   # 添加mykey2

PFCOUNT mykey2   #  查看mykey2中的不重复的数据元素个数

PFMERGE mykey3 mykey mykey2   # 合并mykey和mykey2 , 结果合并到mykey3

PFCOUNT mykey3     # 求mykey3
  1. bitmaps

按位存储
两个状态的都可以使用这个类型
就只有0和1两个状态

#  这里的0 1   就是周一打卡  1 0 就是 周二没打卡 , 第一个数字代表周几  ,第二个数字代表 是否打卡 , 只有两种状态0 和 1
127.0.0.1:6379> SETBIT sign 0 1
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0
(integer) 0
127.0.0.1:6379> SETBIT sign 2 1
(integer) 0
127.0.0.1:6379> SETBIT sign 3 1
(integer) 0
127.0.0.1:6379> SETBIT sign 4 0
(integer) 0
127.0.0.1:6379> SETBIT sign 4 0
(integer) 0
127.0.0.1:6379> SETBIT sign 6 0


# 查看周一是否打卡
127.0.0.1:6379> GETBIT sign 0
(integer) 1
# 查看周二是否打卡
127.0.0.1:6379> GETBIT sign 1
(integer) 0

# 统计打卡记录
BITCOUNT sign

事务

redis事务的本质:一组命令的集合! 一个事务中的所有命令都会被序列化 , 在事务执行的过程中会按照顺序执行!
redis单挑命令是保留原子性的,但是事务不保证原子性!
redis事物没有隔离的概念

所有的命令在事物中并没有被执行!只有复赛执行命令的时候才会执行

一次性、顺序性、排他性

  • 开启事物(Multi)
  • 命令入队(其他命令)
  • 执行事务(exec)
    锁:redis可以实现乐观锁
# 开启事务
127.0.0.1:6379> multi
OK
# 命令入队
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED

# 执行事务
127.0.0.1:6379> exec
1) OK
2) OK
3) OK

# 关闭事务
127.0.0.1:6379> multi    # 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> DISCARD    # 取消事务
OK
127.0.0.1:6379> get key4    # 事务队列中的命令都不会执行
(nil) 

编译型异常

127.0.0.1:6379> MULTI    # 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> mset k2 v2 k3 v3
QUEUED
127.0.0.1:6379> GETSET k3    # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec   # 执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1    # 所有的命令都没有被执行 , 这里get上面set的值也都get不到
(nil)
127.0.0.1:6379> 

运行时异常

127.0.0.1:6379> set k1 "v1"   # 设置一个字符串
OK
127.0.0.1:6379> MULTI      #  开启事务
OK
127.0.0.1:6379> INCR k1    # 让那个字符串自增1 , 这个是运行时异常,命令没有错 , 但是运行时会发现字符串不能自增
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> EXEC   # 虽然第一条命令报错了,但是事务依旧执行成功了
1) (error) ERR value is not an integer or out of range 
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"

乐观锁 , 监控! Watch

悲观锁

  • 很悲观 , 认为什么时候都会出问题 , 无论什么时候都会加锁

乐观锁

  • 很乐观 , 认为什么时候都不会出现问题 , 所以不会上锁 , 更新数据时候会判断一下在此期间是否有人修改过这个数据 version!+
  • 获取version
  • 更新时候比较version

Redis的监视测试

127.0.0.1:6379> set money 100   # 设置money100
OK
127.0.0.1:6379> set out 0     # 设置out 0
OK
127.0.0.1:6379> WATCH money    # 监视money
OK

127.0.0.1:6379> MULTI   # 开启事务   事务正常结束 , 数据期间没有发生变动 , 这时候就正常执行成功
OK
127.0.0.1:6379> DECRBY money 20   # money 减少 20
QUEUED
127.0.0.1:6379> INCRBY out 20   #  out 增加  20
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 80
2) (integer) 20

测试多线程 , 修改完值 , 事务失败
使用watch 可以当做redis的乐观锁操作

127.0.0.1:6379> WATCH money   ##   监视money
OK
127.0.0.1:6379> MULTI    ## 开启事务
OK
127.0.0.1:6379> DECRBY money 10    # 
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> EXEC   # 执行之前 , 另外一个线程修改了我们的money值 , 这个时候就会导致我们事务执行失败
(nil)
127.0.0.1:6379> 
127.0.0.1:6379> UNWATCH   # 如果发现事务执行失败 , 就先解锁
OK
127.0.0.1:6379> WATCH money    # 获取最新的值 ,  再次监视 , select version
OK
127.0.0.1:6379> MULTI   # 开启事务
OK 
127.0.0.1:6379> DECRBY money 10    
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> EXEC   # 比对监视的值有无变化 , 如果没有变化 , 那么可以执行成功 , 如果变化了 , 就执行失败

如果修改失败获取最新的值就可

Jedis

什么是Jedis 是Redis官方推荐的java连接开发工具 , 使用java操作Redis中间件 , 如果使用java操作redis , 那么一定要对Jedis十分熟悉

  1. 导入对应依赖
<dependencies>
     <!--导入Jedis的包-->
     <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
     <dependency>
         <groupId>redis.clients</groupId>
         <artifactId>jedis</artifactId>
         <version>3.2.0</version>
     </dependency>

     <!--导入fastjson的包-->
     <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>fastjson</artifactId>
         <version>1.2.74</version>
     </dependency>
 </dependencies>
  1. 编码测试
  • 连接数据库
  • 操作命令
  • 断开连接
package com.starcpdk;

import redis.clients.jedis.Jedis;

public class TestPing {
    public static void main(String[] args) {
        // 第一步new一个Jedis对象
        Jedis jedis = new Jedis("192.168.174.131" , 6379);

        // jedis 所有的命令就是我们之前学习的所有指令
        System.out.println(jedis.ping());
    }
}

这里测试极有可能会报错 , 大概报错就是连接不上远程redis

redis.clients.jedis.exceptions.JedisConnectionException: Failed connecting to host 192.168.80.128:6379

    at redis.clients.jedis.Connection.connect(Connection.java:207)
    at redis.clients.jedis.BinaryClient.connect(BinaryClient.java:93)
    at redis.clients.jedis.Connection.sendCommand(Connection.java:126)
    at redis.clients.jedis.Connection.sendCommand(Connection.java:117)
    at redis.clients.jedis.Jedis.get(Jedis.java:152)
    at com.redis.study.test.RedisTest.test(RedisTest.java:14)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.net.SocketTimeoutException: connect timed out
    at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
    at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
    at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
    at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
    at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
    at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
    at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
    at java.net.Socket.connect(Socket.java:589)
    at redis.clients.jedis.Connection.connect(Connection.java:184)
    ... 27 more
  • 报这样的错我们首先要考虑到的就是ip地址和端口写对了没
  • 检查代码
  • 然后我们考虑是否是防火墙的问题 , 如果是防火墙的问题我们需要把防火墙关闭
  • 我们接着考虑redis本身的问题
  • 经过检查 , 我们的redis没有 将bind注释掉以及关闭保护模式 , 接下来我们解决这个问题
  • 修改redis.conf配置
    在这里插入图片描述

在这里插入图片描述

SpringBoot整合redis

导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.starcpdk</groupId>
    <artifactId>redis-springboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redis-springboot</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

配置连接

# springboot所有的 配置类都有一个自动配置类   RedisAutoConfiguration
# 自动配置类都会绑定一个properties 配置文件  RedisProperties

# 配置redis
spring.redis.host=192.168.174.131
spring.redis.port=6379
spring.redis.database=0

测试

package com.starcpdk;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
class RedisSpringbootApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {

        // redisTemplate
        // opsForValue  操作字符串的 类似string
        // opsForList 操作list的

        // 除了基本的操作 , 我们常用的方法都可以直接通过redisTemplate操作 , 比如事务 ,  和基本的CRUD

        // 获取连接
        // RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        // connection.flushAll();
        // connection.flushDb();

        redisTemplate.opsForValue().set("mykey" , "yyf");
        System.out.println(redisTemplate.opsForValue().get("mykey"));


    }

}

序列化

我们在User实体类中必须序列化对象 , 也就是实现Serializable接口


@Test
    public void test() throws JsonProcessingException {
        User user = new User("hello", 3);
        String jsonUser = new ObjectMapper().writeValueAsString(user);  // 将user对象序列化为json字符串
        redisTemplate.opsForValue().set("user" , jsonUser);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

自定义配置类


package com.starcpdk.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        // 我们为了自己开发方便 , 一般直接使用<String , Object>
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 序列化配置
        //Json的序列化
        Jackson2JsonRedisSerializer objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL , JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectJackson2JsonRedisSerializer.setObjectMapper(om);
        // String的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String序列化
        template.setKeySerializer(stringRedisSerializer);
        
        // hash的key采用String的序列化
        template.setHashKeySerializer(stringRedisSerializer);
        
        // value采用jackson的序列化
        template.setValueSerializer(objectJackson2JsonRedisSerializer);

        // hash 的value采用Jackson序列化
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
        
        template.afterPropertiesSet();

        return template;
    }
}

自己写一些RedisUtils

package com.starcpdk.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

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

@Component
public final 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;
        }
    }


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


    // ============================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;
        }

    }

}

RedisConf详解

启动的时候通过配置文件启动

网络

bind 127.0.0.1 # 绑定的ip 
protected-mode yes # 保护模式 
port 6379 # 端口设置

通用 GENERAL


daemonize yes # 以守护进程的方式运行,默认是 no,我们需要自己开启为yes! 
pidfile /var/run/redis_6379.pid # 如果以后台的方式运行,我们就需要指定一个 pid 文件!
# 日志 # Specify the server verbosity level. # This can be one of:
# debug (a lot of information, useful for development/testing) 
# verbose (many rarely useful info, but not a mess like the debug level) 
# notice (moderately verbose, what you want in production probably) 生产环境 
# warning (only very important / critical messages are logged)
loglevel notice 
logfile "" # 日志的文件位置名 
databases 16 # 数据库的数量,默认是 16 个数据库 
always-show-logo yes # 是否总是显示LOGO

快照

持久化, 在规定的时间内,执行了多少次操作,则会持久化到文件 .rdb. aof
redis 是内存数据库,如果没有持久化,那么数据断电及失!

# 如果900s内,如果至少有一个1 key进行了修改,我们及进行持久化操作 
save 900 1 
# 如果300s内,如果至少10 key进行了修改,我们及进行持久化操作 
save 300 10 
# 如果60s内,如果至少10000 key进行了修改,我们及进行持久化操作 
save 60 10000
# 我们之后学习持久化,会自己定义这个测试! 

stop-writes-on-bgsave-error yes # 持久化如果出错,是否还需要继续工作! 

rdbcompression yes # 是否压缩 rdb 文件,需要消耗一些cpu资源! 

rdbchecksum yes # 保存rdb文件的时候,进行错误的检查校验! 

dir ./ # rdb 文件保存的目录!

REPLICATION 复制

SECURITY 安全

可以在这里设置redis的密码,默认是没有密码!

127.0.0.1:6379> ping 
PONG 
127.0.0.1:6379> config get requirepass # 获取redis的密码
1) "requirepass" 
2) "" 
127.0.0.1:6379> config set requirepass "123456" # 设置redis的密码 
OK
127.0.0.1:6379> config get requirepass # 发现所有的命令都没有权限了
(error) NOAUTH Authentication required. 
127.0.0.1:6379> ping 
(error) NOAUTH Authentication required. 
127.0.0.1:6379> auth 123456 # 使用密码进行登录! 
OK
127.0.0.1:6379> config get requirepass 
1) "requirepass" 
2) "123456"

限制 CLIENTS

maxclients 10000 # 设置能连接上redis的最大客户端的数量 
maxmemory <bytes> # redis 配置最大的内存容量 
maxmemory-policy noeviction # 内存到达上限之后的处理策略 
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值) 
2、allkeys-lru : 删除lru算法的key 
3、volatile-random:随机删除即将过期key 
4、allkeys-random:随机删除 
5、volatile-ttl : 删除即将过期的 
6、noeviction : 永不过期,返回错误

APPEND ONLY 模式 aof配置

appendonly no # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下, rdb完全够用! 
appendfilename "appendonly.aof" # 持久化的文件的名字 
# appendfsync always # 每次修改都会 sync。消耗性能 
appendfsync everysec # 每秒执行一次 sync,可能会丢失这1s的数据! 
# appendfsync no # 不执行 sync,这个时候操作系统自己同步数据,速度最快!

Redis持久化

RDB持久化

触发机制

1、save的规则满足的情况下,会自动触发rdb规则
2、执行 flushall 命令,也会触发我们的rdb规则!
3、退出redis,也会产生 rdb 文件!
备份就自动生成一个 dump.rdb

如果恢复rdb文件!

只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb 恢复其中的数据!

127.0.0.1:6379> config get dir 
1) "dir" 
2) "/usr/local/bin" # 如果在这个目录下存在 dump.rdb 文件,启动就会自动恢复其中的数据
  • 优点:
    1、适合大规模的数据恢复!
    2、对数据的完整性要不高!
  • 缺点:
    1、需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有的了!
    2、fork进程的时候,会占用一定的内容空间

AOF持久化

将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍!

以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件
但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件
的内容将写指令从前到后执行一次以完成数据的恢复工作

  • 默认是不开启的,我们需要手动进行配置!我们只需要将 appendonly 改为yes就开启了 aof!

  • 重启,redis 就可以生效了!

  • 如果这个 aof 文件有错误,这时候 redis 是启动不起来的吗,我们需要修复这个aof文件

  • redis 给我们提供了一个工具 redis-check-aof --fix

  • 优点:
    1、每一次修改都同步,文件的完整会更加好!
    2、每秒同步一次,可能会丢失一秒的数据
    3、从不同步,效率最高的!

  • 缺点:
    1、相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢!
    2、Aof 运行效率也要比 rdb 慢,所以我们redis默认的配置就是rdb持久化

1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始
的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重
写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式

  • 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF
    文件保存的数据集要比RDB文件保存的数据集要完整。
  • RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者
    建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有
    AOF可能潜在的Bug,留着作为一个万一的手段。

5、性能建议

  • 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够
    了,只保留 save 900 1 这条规则。
  • 如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自
    己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产
    生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite
    的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重
    写可以改到适当的数值。
  • 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也
    减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,
    启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。

Redis发布订阅

在这里插入图片描述

  • 订阅端
127.0.0.1:6379> SUBSCRIBE yyf     #  订阅频道  yyf
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "yyf"
3) (integer) 1

# 等待读取推送信息
1) "message"     # 消息
2) "yyf"        # 哪儿个频道的消息
3) "hello yyf"   #  消息的具体内容
1) "message"
2) "yyf"
3) "hello redis"
  • 发送端
127.0.0.1:6379> PUBLISH yyf "hello yyf"     # 发布一条消息到yyf频道
(integer) 1
127.0.0.1:6379> PUBLISH yyf "hello redis"    # 发布一条消息到yyf频道
(integer) 1
127.0.0.1:6379> 

Redis主从复制

主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用! 一主二从!

主从复制的作用主要包括:
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务
(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用(集群)基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

Redis集群环境配置

只配置从库,不用配置主库!

127.0.0.1:6379> INFO replication     #   查看当前库的信息
# Replication   
role:master    # 角色   master
connected_slaves:0    # 没有从机
master_replid:77c0f291240f0f3314f952975c0788ee0aea53d9
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379> 

复制3个配置文件 , 修改对应信息
1、端口
2、pid 名字
3、log文件名字
4、dump.rdb 名字

修改完毕之后,启动我们的3个redis服务器,可以通过进程信息查看!

在这里插入图片描述

我们要配置的是一主两从
默认情况下,每台Redis服务器都是主节点; 我们一般情况下只用配置从机就好了!
一般情况下只需要配置从机就好了

一主(79)二从(80,81)

在从机进行配置

127.0.0.1:6380> SLAVEOF 127.0.0.1 6379    #  认老大 ,  找谁当老大
OK
127.0.0.1:6380> info replication
# Replication
role:slave      # 当前角色是从机
master_host:127.0.0.1    #   这里是主机的信息
master_port:6379
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:4367d86257a09e481d240c63e14252310a9f095a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
127.0.0.1:6380> 


# 在主机中查看
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1    # 多了从机的配置
slave0:ip=127.0.0.1,port=6380,state=online,offset=28,lag=0   # 这里是从机的信息
master_replid:4367d86257a09e481d240c63e14252310a9f095a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28
127.0.0.1:6379> 

真实的从主配置应该在配置文件中配置,这样的话是永久的,我们这里使用的是命令,暂时的!

主机可以写,从机不能写只能读!主机中的所有信息和数据,都会自动被从机保存!

测试:

主机断开连接,从机依旧可以连接到主机的,但是没有写操作,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息!

如果是使用命令行,来配置的主从,这个时候如果重启了,就会变回主机!只要变为从机,立马就会从主机中获取值!

Slave 启动成功连接到 master 后会发送一个sync同步命令

Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。

  • 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
  • 增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步。

但是只要是重新连接master,一次完全同步(全量复制)将被自动执行! 我们的数据一定可以在从机中看到!

如果没有主节点了, 这时候能不能自己从从节点中选一个主节点

在没有哨兵模式的时候 , 主节点挂掉 , 我们只能自己手动设置

在某个从节点的主机中执行下面的命令 , 就可以自己当主节点了 , 其他的节点就可以手动连接到这个主节点

slaveof no one

如果主机断开了连接,我们可以使用 SLAVEOF no one 让自己变成主机!其他的节点就可以手动连
接到最新的这个主节点(手动)!如果这个时候老大修复了,那就重新连接!

哨兵模式

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。

切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。

我们目前的状态是 一主二从!
1、配置哨兵配置文件 sentinel.conf

# sentinel monitor 被监控的名称 host port 1 
sentinel monitor myredis 127.0.0.1 6379 1

后面的这个数字1,代表主机挂了,slave投票看让谁接替成为主机,票数最多的,就会成为主机!

2、启动哨兵!

[root@localhost bin]# redis-sentinel kconfig/sentinel.conf 
12133:X 05 Feb 2021 17:25:52.362 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
12133:X 05 Feb 2021 17:25:52.362 # Redis version=6.0.6, bits=64, commit=00000000, modified=0, pid=12133, just started
12133:X 05 Feb 2021 17:25:52.362 # Configuration loaded
12133:X 05 Feb 2021 17:25:52.363 * Increased maximum number of open files to 10032 (it was originally set to 1024).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 6.0.6 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 12133
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

12133:X 05 Feb 2021 17:25:52.365 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
12133:X 05 Feb 2021 17:25:52.411 # Sentinel ID is c603cfbdf9717eb8daa156fb621c918fd75e8780
12133:X 05 Feb 2021 17:25:52.411 # +monitor master myredis 127.0.0.1 6379 quorum 1
12133:X 05 Feb 2021 17:25:52.413 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379

如果Master 节点断开了,这个时候就会从从机中随机选择一个服务器! (这里面有一个投票算法!)

如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则!

  • 优点:
    1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
    2、主从可以切换,故障可以转移,系统的可用性就会更好
    3、哨兵模式就是主从模式的升级,手动到自动,更加健壮!
  • 缺点:
    1、Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
    2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择!

哨兵模式的全部配置

# Example sentinel.conf 
# 哨兵sentinel实例运行的端口 默认26379 
port 26379 

# 哨兵sentinel的工作目录 
dir /tmp 

# 哨兵sentinel监控的redis主节点的 ip port 
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。 
# quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了 
# sentinel monitor <master-name> <ip> <redis-port> <quorum> 
sentinel monitor mymaster 127.0.0.1 6379 2 

# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供 密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码 
# sentinel auth-pass <master-name> <password> 
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd 

# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒 
# sentinel down-after-milliseconds <master-name> <milliseconds> 
sentinel down-after-milliseconds mymaster 30000 

# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步, 
这个数字越小,完成failover所需的时间就越长, 
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。 
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。 
# sentinel parallel-syncs <master-name> <numslaves> 
sentinel parallel-syncs mymaster 1 

# 故障转移的超时时间 failover-timeout 可以用在以下这些方面: 
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。 
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那 里同步数据时。 
#3.当想要取消一个正在进行的failover所需要的时间。 
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时, slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了 
# 默认三分钟 
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000 

# SCRIPTS EXECUTION 

#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知 相关人员。 
#对于脚本的运行结果有以下规则: 
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10 
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。 
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。 
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。 

#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等), 将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信 息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配 置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无 法正常启动成功。 
#通知脚本 # shell编程 
# sentinel notification-script <master-name> <script-path> 
sentinel notification-script mymaster /var/redis/notify.sh 

# 客户端重新配置主节点参数脚本 
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已 经发生改变的信息。 
# 以下参数将会在调用脚本时传给脚本: # <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port> # 目前<state>总是“failover”, # <role>是“leader”或者“observer”中的一个。 # 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通 信的# 这个脚本应该是通用的,能被多次调用,不是针对性的。 
# sentinel client-reconfig-script <master-name> <script-path> 
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh 
# 一般都是由运维来配置!

Redis缓存穿透和雪崩

缓存穿透(查不到导致的 )

缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。

解决方案

  • 布隆过滤器

    • 布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
  • 缓存空对象

    • 但是这种方法会存在两个问题:
      1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
      2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿(量太大,缓存过期!)

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大

解决方案

  • 设置热点数据永不过期
    从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。

  • 加互斥锁
    分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大

缓存雪崩

缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

解决方案

  • redis高可用
    这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)

  • 限流降级(在SpringCloud讲解过!)
    这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

  • 数据预热
    数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值