八、Redis

NoSQL
  • NoSQL引言

    • Not Only SQL:不仅仅是SQL,泛指非关系型数据库。Nosql这个技术门类,在2009年趋势越发高涨
    • RDBMS,关系型数据库,sql语句,在并发大时不足以满足需求
    • 传统的关系型数据库实现超大规模和高并发的纯动态网站在性能上不够乐观,如商城网站中对商品数据频繁查询、对热搜商品的排行统计、订单超时等,此时Nosql这个技术门类更好的解决了这些问题;它告诉了时间不仅仅是sql
  • NoSQL的四大分类

    • 键值(Key-Value)存储数据库
      • 这一类数据库主要会使用到哈希表,这个表中有一个特定的键和一个指针指向特定的数据
      • Key-Value模型对于IT系统来说的优势在于简单、易部署,但是如果DBA只对部分值进行查询或更新的时候,Key-Value就显得效率低下
      • 相关产品有
        • Tokyo Cabinet/Tyrant、
        • Redis Key value数据在内存中 运行软件到磁盘,到内存,到操作,所有东西在内存时不需要经过io来读取磁盘数据,故通常认为数据在内存中快
        • SSDB Key value数据在硬盘中;官方说效率不低于内存中
        • Voldemort
        • Oracle BDB
    • 列存储数据库
      • 通常用来应对分布式存储到海量数据
      • 键仍然存在,但是特点是指向了多个列,这些列是由列家族的算法来安排的
        • rouingkey,根据key路由到不同的列中做存储,难点
      • 相关产品
        • Cassandra
        • HBase
        • Riak
    • 文档型数据库,暂时对事物的支持不够好
      • 灵感来自Lotus Notes办公软件,同第一种键值存储相类似,该类型的数据模型是版本化的文档,半结构化的文档以特定的格式存储,比如JSON;文档型数据库可以看作是键值数据库的升级版,允许之间嵌套键值,而且文档型数据库比键值数据库的查询效率更高
        • 如{“id”:“21”,“name”:“mo”,“age”:“18”} 在文档型数据库中称为document
        • JSON更容易的存储一些复杂的数据格式,而且更加灵活,可以只放有数据的,在MongoDB中称为BSON
      • 特点为以文档形式存储
      • 相关产品
        • MongoDB
        • CouchDB
    • 图形(Graph)数据库,主要完成项目中一些二进制存储或存储地图等
      • 与其他行列以及刚性结构的SQL数据库不同,它是使用灵活的图形模型,并且能够扩展到多个服务器上
      • NoSQL数据库没有标准的查询语言(SQL),因此进行数据库查询需要制定数据模型。许多NoSQL数据库都有REST式的数据接口或查询API
      • 相关产品
        • Neo4J
        • InfoGrid
        • Infinite Graph
  • NoSQL应用场景

    • 数据模型比较简单
    • 需要灵活性更强的IT系统(系统在设计中灵活,性能要求较高)
    • 对数据库性能要求较高
    • 不需要高度的数据一致性(NoSQL产品对于事务支持都不是特别良好)
Redis
  • 开源的,遵循BSD许可的,基于内存数据存储,被用于作为数据库缓存消息的中间件

  • Redis数据存在内存中,是一个内存型的数据库,可以做缓存或消息中间件等

    • 最大的优点是读写快
    • 缺点是断电立即消失
    • 机制:持久化机制,内存数据会定期写入到磁盘中,所有对这个缺点弥补一些
  • Redis特点

    • 高性能的Key/Value内存型数据库
    • 支持丰富的数据类型(String,List,Set,ZSet,Hash)
    • 支持持久化,内存数据持久到硬盘中
    • 设计时时单进程,单线程;不支持并发,线程安全,不需要加锁,借鉴了Memechache多线程处理时内部的锁机制,效率相对于Memechache高;在开发中通常使用Redis解决分布式锁
  • 安装

    • 解压缩

      tar -zxvf redis-4.0.11.tar.gz
      
    • 源码安装

      有Makefile,即可以直接make执行,编译安装,需要有C语言环境(crc16算法)
      在Makefile目录下: 
      make MALLOC=libc
      
      PREFIX指定安装目录
      make install PREFIX=/usr/redis
      
      redis-cli是客户端
      
      启动redis
      ./redis-server 
      
      另起窗口连接redis
      ./redis-cli -h localhost -p 6379 				//本机上连接可以不指定localhost和-p
      连接时加上--raw,可以让终端展示中文
      ./redis-cli -h localhost -p 6379 --raw
      
  • Redis常用操作指令

    • redis启动服务的细节

      • 直接使用 ./redis-server方式启动使用的是redis-server这个shell脚本中的默认配置
    • 启动redis时指定配置文件启动

      • 默认在redis安装完成后在安装目录没有任何配置文件,需要在源码目录中复制redis.conf配置文件到安装目录

        进入redis-4.0.11源码目录,拷贝redis.conf到/user/redis
        cp redis.conf /usr/redis/
        
        进入bin目录加载配置启动,可以写相对路径
        ./redis-server ../redis.conf
        
        修改redis默认端口号为7000
        redis.conf中的 port 7000
        
    • redis中的库,类似于关系型数据库

      • 库:database,用来存放数据的一个基本单元,一个库中可以存放大量的key-value键值对

      • 每一个库都有一个唯一的名字,或称为编号,从0开始

      • redis中默认库的个数为16个,编号从0到15,默认使用0号库

      • 切换库

        select dbid(库编号)			库与库之间相互隔离,不同库之间可以用相同的key
        
    • redis中清除库的指令

      FLUSHDB		清空当前库
      FLUSHALL	 清空所有库
      
  • redis操作key相关指令

    • DEL指令

      语法:DEL key1 key2 ...
      作用:删除给定的一个或多个key,不存在的key会被忽略
      可用版本 1.0.0以上
      返回值:被删除key的数量
      
    • EXISTS指令

      语法:EXISITS key		当指定多个key时,有一个key存在就返回1
      作用:检查给定的key是否存在
      可用版本 1.0.0以上
      返回值:存在返回1,否则返回0
      
    • EXPIRE:解决超时数据问题

      语法:EXPIRE key seconds		单位默认为秒,expire name 10
      作用:为给定key设置生存时间,当key生存时间为0(过期)时,会自动删除
      可用版本 1.0.0以上
      时间复杂度:O(1)
      返回值:设置成功返回1
      
    • KEYS

      语法:KEYS pattern 
      作用:查找所有符合给定模式pattern 的key
      	KEYS *:匹配数据库中所有key
      	KEYS h?llo	匹配hello,hallo等,一个字符
      	KEYS h*llo	通配符,不限数量
      	KEYS [ae]llo	匹配hello和hallo,特殊符号用"\"隔开,[]代表匹配一个,可以多个中括号
      可用版本 1.0.0以上
      返回值:符合给定模式的key列表
      
    • MOVE

      语法:MOVE key db
      作用:将当前数据等key移动到给定的数据库db中,本身库就没有了
      可用版本 1.0.0以上
      返回值:移动成功返回1,失败返回0
      
    • PEXPIRE

      语法:PEXPIER key milliseconds
      作用:和EXPIRE作用类似,单位为毫秒设置key的生存时间
      可用版本 1.0.0以上
      时间复杂度:O(1)
      返回值:成功返回1,key不存在或设置失败返回0
      
    • PEXPIREAT

      语法:PEXPIREAT key milliseconds-timestamp
      作用:类似与EXPIREAT,以毫秒为单位设置key的过期unix时间戳,不
      可用版本 2.6.0以上
      返回值:如果生存时间设置成功返回1,key不存在或没办法设置生存时间时返回0
      
    • TTL

      语法:TTL key
      作用:以秒为单位,返回给定key的剩余生存时间(TTL,time to live)
      可用版本 1.0.0以上
      返回值:key不存在返回-2,key存在但没有设置剩余生存时间返回-1,其他以秒为单位返回key的剩余生存时间
      Note:在Redis2.8以前,当key不存在或key没有设置生存时间时,命令都返回-1
      
    • PTTL

      语法:PTTL key
      作用:类似于TTL,以毫秒为单位返回key
      可用版本 2.6.0以上
      返回值:key不存在返回-2,key存在但没有设置剩余生存时间返回-1,其他以毫秒为单位返回key的剩余生存时间
      Note:在Redis2.8以前,当key不存在,或key没有设置剩余生存时间时,命令都返回-1
      
    • RANDOMKEY

      语法:RANDOMKEY
      作用:从当前数据库中随机返回(不删除)一个key
      可用版本 1.0.0以上
      返回值:当数据库不为空时,返回一个key,当数据库为空时,但会null
      
    • RENAME

      语法:RENAME key newkey
      作用:将key改名为newkey,当key和newkey相同时,或key不存在时,返回一个错误;当newkey已经存在时,RENAME命令将覆盖旧值
      可用版本 1.0.0以上
      返回值:改名成功提示OK,失败返回一个错误
      
    • TYPE

      语法:TYPE key
      作用:返回key所存储的值的类型
      可用版本 1.0.0以上
      返回值:
      	none:key不存在
      	string:字符串
      	list:列表
      	set:集合
      	zset:有序集
      	hash:哈希表
      
Redis中String类型
  • 内存存储模型

    • key为String,value为String
  • 常用操作命令

    命令说明
    set设置一个key/value
    get根据key获取对应的value
    mset一次设置多个key/value
    mget一次获得多个key的value
    getset获取原始key的值,同时设置新值
    strlen获取对应key存储value的长度
    append为对应key的value追加内容
    getrange索引0开始截取value的内容
    setex设置一个key存活的有效期(秒)
    psetex设置一个key存活的有效期(毫秒)
    setnx存在不做任何操作,不存在添加
    msetnx原子操作(只要有一个存在不做任何操作)可以同时设置多个key,只要有一个存在都不保存
    decr进行数值类型的-1操作
    decrby根据提供的数据进行减法操作
    incr进行数值类型的+1操作
    incrby根据提供的数据进行加法操作
    incrbyfloat根据提供的数据加入浮点数
  • 操作演示

    //mset
    mset name mo age 18 bir 1994-08-03
    
    //mget
    mget name age bir
    
    //getset
    127.0.0.1:7000> getset name yuan
    "mo"
    127.0.0.1:7000> get name
    "yuan"
    
    //strlen 长度
    strlen name
    
    //append
    127.0.0.1:7000> append name  love
    
    //getrange
    127.0.0.1:7000> getrange name 4 8
    "love"
    127.0.0.1:7000> getrange name 4 -1	//-1代表末尾
    "love"
    
    //setex
    127.0.0.1:7000> setex age 100 18
    OK
    127.0.0.1:7000> ttl age
    (integer) 94
    
    //decr
    127.0.0.1:7000> get age
    "19"
    127.0.0.1:7000> decr age
    (integer) 18
    
    //decrby
    127.0.0.1:7000> decrby age 10
    (integer) 8
    
    //incr
    127.0.0.1:7000> incr age
    (integer) 9
    
    //incrbyfloat		精度为17位,不是四舍五入
    127.0.0.1:7000> incrbyfloat age 1.23234
    "10.23234"
    
Redis的List类型
  • 内存存储模型

    • key为String,值为列表(list)类型,有序可重复,可以从表头(lpush)或表尾(rpush)插入
  • 常用操作指令

    命令说明
    lpush将某个值加入到一个key列表头部,列表不存在时创建
    lpushx同lpush,但是必须保证这个key存在,无法创建列表
    rpush将某个值加入到一个key列表末尾,列表不存在时创建
    rpushx同rpush,但是必须保证这个kye存在,无法创建列表
    lpop返回和移除列表左边的第一个元素
    rpop返回和移除列表右边的第一个元素
    lrange 0 -1获取某一个下表区间内的元素
    llen获取列表元素个数
    lset设置某一个指定索引的值(索引必须存在)
    lindex获取某一个指定索引位置的元素
    lrem删除重复元素
    ltrim保留列表中特定区间内的元素
    linsert在某一个元素之前,之后插入新元素
  • 命令演示

    //创建并放入
    127.0.0.1:7000> lpush user mo yuan hang
    (integer) 3
    127.0.0.1:7000> keys *
    1) "user"
    2) "name"
    3) "bir"
    4) "age"
    127.0.0.1:7000> type user
    list
    
    //遍历
    127.0.0.1:7000> lrange user 0 -1
    1) "hang"
    2) "yuan"
    3) "mo"
    
    //从末尾添加
    127.0.0.1:7000> rpush user erha
    (integer) 4
    //遍历
    127.0.0.1:7000> lrange user 0 -1
    1) "hang"
    2) "yuan"
    3) "mo"
    4) "erha"
    127.0.0.1:7000> 
    
    //lset
    127.0.0.1:7000> lset user 3 master
    OK
    127.0.0.1:7000> lrange user 0 -1
    1) "hang"
    2) "yuan"
    3) "mo"
    4) "master"
    
    //lindex
    127.0.0.1:7000> lindex user 3
    "master"
    
    lrem user 2 mo			想删除几个重复的元素mo,默认从左边开始匹配
    
    //ltrim
    127.0.0.1:7000> ltrim user 0 2
    OK
    127.0.0.1:7000> lrange user 0 -1
    1) "hang"
    2) "yuan"
    3) "mo"
    
    //linsert		默认从左开始,before在之前插入,after在之后插入
    127.0.0.1:7000> linsert user before mo master
    (integer) 4
    127.0.0.1:7000> lrange user 0 -1
    1) "hang"
    2) "yuan"
    3) "master"
    4) "mo"
    
Redis中的Set类型
  • 内存存储模型

    • key为String,value为Set集合,无序,不可重复
  • 常用操作指令

    命令说明
    sadd为集合添加元素
    smembers显示集合中的所有元素,无序
    scard返回集合中元素的个数
    spop随机返回一个元素并将元素在集合中删除
    smove从一个集合中向另一个集合移动元素,必须是同一种类型集合
    srem从集合中删除一个元素
    sismember判断一个集合中是否含有这个元素
    srandmember随机返回元素,不会删除元素
    sdiff只展示,不删除,去掉第一个集合中其他集合含有的相同元素
    sinter求交集
    sunion求和集
  • 命令演示

    127.0.0.1:7000> smembers sets
    1) "hang"
    2) "yuan"
    3) "mo"
    4) "kun"
    5) "xin"
    127.0.0.1:7000> sadd ages 18 19 20
    (integer) 3
    
    //smove
    127.0.0.1:7000> smove sets ages kun			//移动过后sets中就没了
    (integer) 1
    127.0.0.1:7000> smembers sets
    1) "yuan"
    2) "mo"
    3) "hang"
    4) "xin"
    127.0.0.1:7000> smembers ages
    1) "19"
    2) "kun"
    3) "20"
    4) "18"
    
    //srem  可以删除多个
    127.0.0.1:7000> srem ages 20 kun
    (integer) 2
    
    //srandmember	可以指定随机返回的个数,不指定则每次返回一个,不会删除元素
    127.0.0.1:7000> srandmember sets 2
    1) "xin"
    2) "mo"
    
    //sdiff
    sdiff	sets1	 sets2		展示,不会删除原集合中的数据;第二个集合为参数,去掉第一个集合中第二个集合中含有的相同元素,第三个同理
    
Redis中的ZSet类型 — 做排行榜、热搜榜
  • 内存模型

    • 类似于Java中的treeSet、sortSet,key是String,value为set,存储时给每个元素绑定一个数字,称之为分数,故有人也称为sortSet;特点为无序,不可重复,但可排序的set集合,排序靠分数实现
    • 给value赋值时要给一个元素还要指定一个元素对应的得分
  • 常用命令

    命令说明
    zadd添加一个有序集合元素
    zcard返回集合的元素个数
    zrange升序zrevrange降序返回一个范围内的元素
    zrangebyscore按照分数查找一个范围内的元素
    zrank返回排名,默认升序
    zrevrank倒序排名,即降序
    zscore显示某一个元素的分数
    zrem移除某一个元素
    zincrby给某个特定元素加分
  • 命令演示

    //zadd
    127.0.0.1:7000> zadd zsets 10 mo 8 hang 9 yuan 12 hang
    (integer) 3
    
    //zrange	根据下标从0到-1,即到最后	 是根据分数给下标排序		根据分数升序展示
    127.0.0.1:7000> zrange zsets 0 -1
    1) "yuan"
    2) "mo"
    3) "hang"
    
    //查询并展示分数
    127.0.0.1:7000> zrange zsets 0 -1 withscores
    1) "yuan"
    2) "9"
    3) "mo"
    4) "10"
    5) "hang"
    6) "12"
    
    //zrangebyscore		根据分数在0到20之间展示,结果展示分数,并分页,第一个参数是第几页,第二个参数是几个
    127.0.0.1:7000> zrangebyscore zsets 0 20 withscores limit 0 10
    1) "yuan"
    2) "9"
    3) "mo"
    4) "10"
    5) "hang"
    6) "12"
    
    //zincrby
    127.0.0.1:7000> zincrby zsets 1 mo
    "11"
    127.0.0.1:7000> zincrby zsets -1 mo
    "10"
    
Redis中的hash类型
  • 内存模型

    • 类似于java中的map集合,key为String,value又是key-value;特点即value是一个map结构,key无序唯一
  • 常用命令

    命令说明
    hset设置一个key/value对
    hget获得一个key对应的value
    hgetall获得所有的key/value对
    hdel删除某一个key/value对
    hexists判断一个key是否存在
    hkeys获得所有的key
    kvals获得所有的value
    hmest设置多个key/value
    hmget获得多个key的value
    hsetnx设置一个不存在的key的值,key不存在添加,存在则不做操作
    hincrby为value进行加法运算
    hincrbyfloat为value加入浮点值
  • 命令演示

    
    //hexists
      127.0.0.1:7000> hexists maps bir
      (integer) 0
      127.0.0.1:7000> hexists maps name
      (integer) 1
    
      //hkeys		hvals
      127.0.0.1:7000> hkeys maps
      1) "name"
      2) "age"
      127.0.0.1:7000> hvals maps
      1) "mo"
      2) "18"
    
      //hmset		hmget
      127.0.0.1:7000> hmset maps add beijing mas sla
      OK
      127.0.0.1:7000> hmget maps add mas
      1) "beijing"
      2) "sla"
    
      //hincrby必须指定每次自增数量
      127.0.0.1:7000> hincrby maps age 1
    (integer) 19
    
      //17位精度
      127.0.0.1:7000> hincrbyfloat maps age 1.2
      "20.2"//hset	第一个key告诉redis,第二个key为value的key
    127.0.0.1:7000> hset maps name mo
    (integer) 1
    127.0.0.1:7000> hset maps age 18
    (integer) 1
    127.0.0.1:7000> hset maps bir 1994-08-03
    (integer) 1
    127.0.0.1:7000> type maps
    hash
    
    //hget		两层key
    127.0.0.1:7000> hget maps name
    "mo"
    
    //hgetall
    127.0.0.1:7000> hgetall maps
    1) "name"
    2) "mo"
    3) "age"
    4) "18"
    5) "bir"
    6) "1994-08-03"
    
    //hdel 删除value中的一个键值对
    127.0.0.1:7000> hdel maps bir
    (integer) 1
    127.0.0.1:7000> hgetall maps
    1) "name"
    2) "mo"
    3) "age"
    4) "18"
    
    //hexists
    127.0.0.1:7000> hexists maps bir
    (integer) 0
    127.0.0.1:7000> hexists maps name
    (integer) 1
    
    //hkeys		hvals
    127.0.0.1:7000> hkeys maps
    1) "name"
    2) "age"
    127.0.0.1:7000> hvals maps
    1) "mo"
    2) "18"
    
    //hmset		hmget
    127.0.0.1:7000> hmset maps add beijing mas sla
    OK
    127.0.0.1:7000> hmget maps add mas
    1) "beijing"
    2) "sla"
    
    //hincrby必须指定每次自增数量
    127.0.0.1:7000> hincrby maps age 1
    (integer) 19
    
    //17位精度
    127.0.0.1:7000> hincrbyfloat maps age 1.2
    "20.2"
    
远程连接
  • Redis服务器默认不允许远程连接

  • 修改配置,开启Redis的远程连接

    • vi redis.conf

          修改netword下的
          bind 127.0.0.1
          
          修改为
          bind 0.0.0.0	代表允许一切客户端连接
          
          修改完重启redis
      

持久化机制
  • 客户端操作Redis时,数据始终在内存中,官方支持把数据从内存保存到磁盘里
  • Redis官方提供了两种不同的持久化方法来将数据存储到硬盘里面
    • 快照(Snapshot)
    • AOF(Append Only File),只追加日志文件
快照 — Snapshot
  • 保存这一时刻的数据状态

  • 将某一时刻的所有数据都写入到硬盘中,也是Redis默认开启的持久化方法,保存的文件是以.rdb形式结尾的,因此这种方式也称为RDB方式

  • 快照生成方式

    • 客户端方式

      • BGSAVE:可以使用BGSAVE命令来创建一个快照,当接受到客户端的BGSAVE命令时,Redis会调用fork来创建一个子进程,然后子进程负责将快照写入磁盘中,而父进程则继续处理命令请求
        • fork:当一个进程创建子进程的时候,底层的操作系统会创建该进程的一个副本,在类unix系统中创建子进程的操作会进行优化;在刚开始的时候,父子进程共享相同内存,直到父进程或子进程对内存进行了写之后,对被写入的内存的共享才会结束服务;
        • 子进程负责生成快照,父进程继续处理客户端请求;调用fork来实现,在不进行写操作时,因为共享内存,所以子进程可以独占大量内存来处理快照,加快效率;最大优点是不会阻塞服务,官方推荐
      • SAVE:创建一个快照,接收到SAVE命令的Redis服务器在快照创建完毕之前将不再响应任何其他的命令
        • Redis主进程负责快照生成,客户端请求在快照完成之前阻塞
        • 并不常用,使用SAVE命令在快照创建完毕之前,Redis处于阻塞状态,无法对外服务
    • 服务器配置自动触发,默认使用的BDSAVE

      • 可以在redis.conf中设置save配置选项,redis会在save选项条件满足之后自动触发一次BGSAVE命令;如果设置多个save配置选项,当任意一个save配置选项条件满足,Redis也会触发一次BGSAVE

        ##################### SNAPSHOTTING  #####################
        //默认:秒,改变
        save 900 1		//900秒以内有一个key发生变化就做一个BGSAVE
        save 300 10			//如果300秒内有十个key发生变化就做一个BGSAVE
        save 60 10000			//如果60秒内有10000个key发生变化就做一个BGSAVE
        
    • 当Redis通过shutdown指令接收到关闭服务器的请求时,会执行一个save命令,阻塞所有的客户端,不再执行客户端执行发送的任何命令,并且在save命令执行完毕后关闭服务器

    • 配置生成快照名称和位置

      redis.conf中:
      修改生成快照名称
      dbfilename dump.rdb		后缀一定要 .rdb
      
      修改生成位置
      dir ./       放置aof和rdb的地方,./ 为当前目录
      
AOF
  • 将所有客户端执行的写命令记录到日志文件中,AOF持久化会将被执行的写命令写到AOF的文件末尾,以此来记录数据发生的变化;因此只要Redis从头到尾执行一次AOF文件所包含的所有写命令,就可以恢复AOF文件的记录的数据集

  • 默认AOF是关闭的

  • 开启AOF持久化

    配置redis.conf
    ################### APPEND ONLY MODE ##################
    
    //默认:
    appendonly no
    appendfilename "appendonly.aof"    	默认生成aof的文件名,放置位置由快照dir决定
    
    //修改:
    appendonly yes
    
  • 日志追加频率

    redis.conf
    ########### APPEND ONLY MODE ######################
    
    //默认
    # appendfsync always
    appendfsync everysec		每秒同步一次
    # appendfsync no
    
    • 解释

      appendfsync always:谨慎使用
      	每个Redis写命令都要同步写入硬盘,严重降低Redis速度;
      	响应的将发生系统崩溃时出现的数据丢失减到最少;这种同步策略需要对硬盘进行大量的写入操作,所以Redis处理命令的速度会受到硬盘性能的限制
      	转盘式(机械式)硬盘在这种频率下200左右个命令 /s,固态硬盘SSD几百万个命令 /s
      	这种模式不断写入数据的做法,有可能引发严重的写入放大问题,导致固态硬盘的寿命从原来的几年降低为几个月
      	
      appendfsync everysec:推荐
      	每秒执行一次同步,显示的将多个写命令同步到磁盘
      	兼顾数据安全和写入性能,让Redis每秒一次的频率对AOF文件进行同步;Redis每秒同步一次AOF文件时性能和不使用任何持久化特性时的性能相差无几,而通过每秒同步一次AOF文件,Redis可以保证,即使系统崩溃,用户最多丢失一秒之内产生的数据
      	
      appendfsync no:不推荐
      	由操作系统决定何时同步
      	使用no选项,将完全由操作系统决定什么时候同步AOF日志文件,这个选项不会对Redis性能带来影响,但是系统崩溃时,会丢失不定数量的数据;另外如果用户硬盘处理写入操作不够快的话,当缓冲区被等待写入硬盘数据填满时,Redis会处于阻塞状态,并导致Redis的处理命令请求的速度变慢
      
AOF文件的重写
  • AOF的方式也带来了另一个问题,持久化文件会变的越来越大;例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实99条都是多余的;因为要恢复数据库的状态,其实文件中保存一条set test 100就够了。为了压缩aof的持久化文件,Redio提供了AOF重写(ReWriter)机制

  • AOF重写:用来在一定程度上减小AOF文件的体积

  • 触发重写方式

    客户端方式触发重写
    	执行BGREWRITEAOF,不会阻塞Redis服务
    
    服务器配置方式自动触发
    	配置redis.conf中的auto-aof-rewrite-percentage
    	如果设置auto-aof-rewrite-percentage值为100和auto-aof-rewriter-min-size 64mb,并且启动AOF持久化时,那么当AOF文件体积大于64M,并且AOF文件的体积比上一次重写之后体积大了至少一倍(percentage,100%)时会自动触发;如果重写过于频繁,用户可以考虑将auto-aof-rewrite-percentage设置为更大
    
  • 重写原理

    • 重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,替换原有的文件;这点和快照有点类似

    • 重写流程

      	Redis调用fork,现在有父子两个进程,子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
      	父进程继续处理client请求,除了把写命令写入到原本的aof文件中,同时把收到的写命令缓存起来,这样就能保证如果子进程重写失败的话不会出现问题
      	当子进程把快照内容以命令方式写入到临时文件后,子进程发信号通知父进程,然后父进程把缓存的写命令也写入到临时文件
      	然后父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的命令也开始往新的aof文件中追加
      
  • 两种持久化方案既可以同时使用,又可以单独使用,在某种情况下也可以都不使用,具体情况取决于用户的数据和应用决定

  • 无论是AOF还是快照机制持久化,将数据持久化到硬盘都是有必要的,除了持久化外,用户还应该对持久化的文件进行备份(最好备份在多个不同的地方)

Java操作Redis
  • 引入依赖

    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
    
  • 创建jedis对象

    public static void main(String[] args){
    	//创建jedis对象
    	//Redis服务必须关闭防火墙,Redis服务必须开启远程访问连接
    	Jedis jedis = new Jedis("192.168.40.4",6379);
    	//选择操作的库默认0号库
    	jedis.select(0);
    	//执行相关操作
    	...
    	...
    	//释放资源
    	jedis.close();
    }
    
  • java中操作Redis

    // 测试redis连接
    public class TestRedis {
        public static void main(String[] args) {
            //创建jedis客户端对象      不写参数默认为本机Redis和默认端口
            Jedis jedis = new Jedis("10.211.55.18", 6379);
            //选择使用一个库  不给参数默认使用 0号库
            jedis.select(0);
    
            //获取redis中的key信息
            Set<String> keys = jedis.keys("*");
            keys.forEach(key -> System.out.println("key="+key));
    
            //操作库相关
            //jedis.flushDB();    //清空当前库
            jedis.flushAll();       //清空所以库
    
            //释放资源
            jedis.close();
        }
    }
    
  • 测试与key相关的部分命令

    public class TestKey {
        private Jedis jedis;
    
        @Before
        public void before(){
            jedis = new Jedis("10.211.55.18", 6379);
        }
    
        @After
        public void after(){
            jedis.close();
        }
    
        //测试key相关
        @Test
        public void testKeys(){
            //删除一个key
            jedis.del("name ");
            //删除多个key
            jedis.del("name", "age");
            //判断一个key是否存在
            Boolean name = jedis.exists("name");
            System.out.println(name);
            String a = jedis.set("age", "18");
            System.out.println(a);
            //设置一个key的超时时间expire    pexpire
            Long age = jedis.expire("age", 1000);   //返回1为设置成功,0为失败
            System.out.println(age);
    
            //查看一个key超时时间 ttl
            Long age1 = jedis.ttl("age");
            System.out.println(age1);
    
            //随机获取一个key
            String s = jedis.randomKey();
            System.out.println(s);
    
            //修改key名称
            String rename = jedis.rename("age", "newAge");
            System.out.println(rename);
    
            //查看key对应值的类型
            String name1 = jedis.type("name");      //没有key的话返回none
            System.out.println(name1);
            String maps = jedis.type("maps");
            System.out.println(maps);
    
        }
    }
    
  • 测试String

    public class TestString {
        private Jedis jedis;
    
        @Before
        public void before(){
            this.jedis = new Jedis("10.211.55.18", 6379);
        }
        @After
        public void after(){
            jedis.close();
        }
        //测试String相关
        @Test
        public void testString(){
            //set
            jedis.set("name", "yuan");
            //get
            String name = jedis.get("name");
            System.out.println(name);
            //mset
            jedis.mset("content", "erha", "address", "西安");
            //mget
            List<String> mget = jedis.mget("name", "content", "address");
            mget.forEach(v-> System.out.println("v="+v));
            //getset
            String set = jedis.getSet("name", "mo");    //获取原始值并更改
            System.out.println(set);
        }
    }
    
  • 测试List

    public class TestList {
        private Jedis jedis;
        @Before
        public void before(){
            this.jedis = new Jedis("10.211.55.18",6379);
        }
        @After
        public void after(){
            jedis.close();
        }
        //测试List相关
        @Test
        public void testList(){
            // lpush
            jedis.lpush("names1", "mo", "yuan", "hang");
            // rpush
            jedis.rpush("names1", "kun");
            // lrange
            List<String> names1 = jedis.lrange("names1", 0, -1);
            names1.forEach(name-> System.out.println("name : "+name));
    
            // lpop rpop
            String names11 = jedis.lpop("names1");
            System.out.println(names11);
    
            // llen     多一个参数,枚举类型,在mo前插入yuan;After与之相反
            jedis.linsert("lists", BinaryClient.LIST_POSITION.BEFORE,"mo","yuan");
        }
    }
    
SpringBoot整合Redis
  • Spring Boot Data Redis 中提供了RedisTemplate和StringRedisTemplate两个对象,其中StringRedisTemplate是RedisTemplate的子类,两个方法基本一致,不同之处主要体现在操作的数据类型不同,RedisTemplate中的两个泛型都是Object,即存储的key和value都可以是一个对象;StringRedisTemplate两个泛型都是String,即StringRedisTemplate的两个key和value都只能是字符串

  • 原生的,针对java操作Redis,想要更复杂的操作,往里面放对象的话,需要使用对象序列化放入到Redis中

  • Spring Data提供了RedisTemplate;可以自动序列化和自动反序列化,在使用RedisTemplate时默认是将对象序列化到Redis中,所以放入的对象必须实现对象序列化接口

  • 环境准备

    • 引入依赖:底层封装集成了jedis,所以不需要再引入jedis

      <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>
      
    • 配置application.properties

      spring.redis.host=localhost
      spring.redis.port=6379
      spring.redis.database=0
      
  • 演示StringRedisTemplate部分API

    • 操作Redis中key相关

      //启动springboot应用
      @SpringBootTest(classes = RedisDay2Application.class)
      @RunWith(SpringRunner.class)
      public class TestStringRedisTemplate {
          //注入StringRedisTemplate
          @Autowired
          private StringRedisTemplate stringRedisTemplate;    //key和value都是字符串
      
          //操作Redis中key相关
          @Test
          public void testKey(){
              stringRedisTemplate.delete("name");     //删除一个key
              Boolean hasKey = stringRedisTemplate.hasKey("name");//判断某个key是否存在
              System.out.println(hasKey);
              DataType dataType = stringRedisTemplate.type("name");//判断key所对应值的类型
              System.out.println(dataType);
              Set<String> keys = stringRedisTemplate.keys("*");   //获取Redis中所有key
              keys.forEach(key-> System.out.println("key : " + key));
              Long expire = stringRedisTemplate.getExpire("name");    //获取key超时时间  -1永不超时,-2不存在,大于等于0为超时时间
              System.out.println(expire);
              stringRedisTemplate.randomKey();        //在Redis中随机获取一个key
              //stringRedisTemplate.rename("age", "age1");  //修改key的名字  要求key必须存在,不存在报错
              //stringRedisTemplate.renameIfAbsent("age","age1");   //修改key的名字,并且判断key是否存在
      
              //stringRedisTemplate.move("name",1); //移动key到指定库
          }
        
      }
      
    • 操作Redis中字符串 opsForValue

      @Test
      public void testString(){
              stringRedisTemplate.opsForValue().set("name", "mo");    //用来设置一个String类型的key-value
              String value = stringRedisTemplate.opsForValue().get("name");
              System.out.println("value : "+value);
              /**
               * 第一二个参数是key-value
               * 第三个是超时时间
               * 第四个是时间单位,TimeUnit是一个枚举类型的类,SECONDS是秒
               */
              stringRedisTemplate.opsForValue().set("code","4264",120, TimeUnit.SECONDS);    //设置一个key的超时时间
      
              stringRedisTemplate.opsForValue().append("name","追加");  //  追加
      }
      
    • 操作Redis中List类型 opsForLIst

      @Test
      public void testList(){
              stringRedisTemplate.opsForList().leftPush("names", "yuan"); //创建一个列表并放入一个元素
              stringRedisTemplate.opsForList().leftPushAll("names","yuan","hang","mo");   //创建一个列表并放入多个元素
              List<String> names = new ArrayList<>();
              names.add("win7");
              stringRedisTemplate.opsForList().leftPushAll("names", names);   //传入集合,存储到Redis中
      
              List<String> stringList = stringRedisTemplate.opsForList().range("names", 0, -1);   //遍历 List
              stringList.forEach(value-> System.out.println("value : "+ value));
      
              stringRedisTemplate.opsForList().trim("names", 1, 4);   //截取指定区间的 List
      }
      
    • 操作Redis中的Set类型 opsForSet

      @Test
      public void testSet(){
              stringRedisTemplate.opsForSet().add("sets", "mo", "yuan", "hang");  //创建set并放入多个元素
              Set<String> sets = stringRedisTemplate.opsForSet().members("sets");     //查看set中成员
              sets.forEach(value-> System.out.println("value : " + value));
              Long size = stringRedisTemplate.opsForSet().size("sets");//获取set集合元素个数
              System.out.println("size = " + size);
      }
      
    • 操作Redis中的Zset opsForZSet

      @Test
      public void testZSet(){
              stringRedisTemplate.opsForZSet().add("zsets", "yuan", 20);    //创建并放入元素
              Set<String> zsets = stringRedisTemplate.opsForZSet().range("zsets", 0, -1);//指定范围查询
              zsets.forEach(value-> System.out.println("value = " + value));
      
              Set<ZSetOperations.TypedTuple<String>> zsets1 = stringRedisTemplate.opsForZSet().rangeByScoreWithScores("zsets", 0, 100);   //获取指定元素以及分数
              zsets1.forEach(typedTuple->{
                  System.out.println(typedTuple.getValue());  //值
                  System.out.println(typedTuple.getScore());  //分数
              });
      }
      
    • 操作Redis中的hash opsForHash

      @Test
      public void testHash(){
              stringRedisTemplate.opsForHash().put("hashs", "name", "mo");    //创建一个Hash类型,并放入key-value
      
              Map<String, String> map = new HashMap<>();
              map.put("age", "12");
              map.put("bir", "1994-08-03");
              stringRedisTemplate.opsForHash().putAll("hashs",map);   //放入多个key-value
      
              List<Object> values = stringRedisTemplate.opsForHash().multiGet("hashs", Arrays.asList("name", "age"));//获取多个key的值
              values.forEach(value-> System.out.println("values = " + value));
      
              String o = (String) stringRedisTemplate.opsForHash().get("hashs", "name");//获取hash中某个key的值
              System.out.println("-------------------\t"+o);
      
              List<Object> maps = stringRedisTemplate.opsForHash().values("hashs");//获取所有values
              maps.forEach(v-> System.out.println("v = " + v));
      
              Set<Object> keys = stringRedisTemplate.opsForHash().keys("hashs");//获取所有keys
              keys.forEach(k-> System.out.println("k = " + k));
      }
      
  • RedisTemplate有关序列化

    //启动springboot应用
    @SpringBootTest(classes = RedisDay2Application.class)
    @RunWith(SpringRunner.class)
    public class TestRedisTemplate {
        //注入RedisTemplate Key Object Value Object   ---> 默认key和value都会经过jdk对象序列化
        @Autowired
        private RedisTemplate redisTemplate;
    
        //opsForXXX     Value   String  List    Set ZSet    Hash
        @Test
        public void testRedisTemplate(){
            /**
             * RedisTemplate对象中key和value的序列化都是jdk序列化,因为jdk序列化允许key和value都能放对象
             * 开发中通常key为String,value为Object;所有需要修改key序列化方案,换成String序列化,即StringRedisSerializer
             */
            //修改key序列化方案
            redisTemplate.setKeySerializer(new StringRedisSerializer());
    
            redisTemplate.opsForValue().set("user",new User(UUID.randomUUID().toString(),"mo",23,new Date()));  //Redis进行设置,对象需要经过序列化
    
            User user = (User) redisTemplate.opsForValue().get("user");
            System.out.println(user);
    
            redisTemplate.opsForList().leftPush("list",user);   //key为String,value为object
    
            redisTemplate.opsForSet().add("set", user);
    
            redisTemplate.opsForZSet().add("zset", user, 10);
    
            //修改Hash key的序列化方案
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.opsForHash().put("map","name",user);  //需要改两个key序列化方案
        }
    }
    
  • 对同一个key的多次操作:Bound

    @SpringBootTest(classes = RedisDay2Application.class)
    @RunWith(SpringRunner.class)
    public class TestBoundAPI { //绑定的API
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        //Spring data 为了方便我们对Redis进行更友好对操作,因此提供了bound api简化操作
        @Test
        public void testBound(){
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    
            //redisTemplate / stringRedisTemplate
            stringRedisTemplate.opsForValue().set("name", "mo");    //每次进行操作都需要带着key
            stringRedisTemplate.opsForValue().append("name", "CYY");    //如果要对一个key进行多次操作
            String s = stringRedisTemplate.opsForValue().get("name");//SpringData可以进行对key对绑定操作
            System.out.println(s);
    
            //对于字符串类型对key进行绑定,后续所有的操作都是基于这个key的操作
            BoundValueOperations<String, String> nameValueOpretions = stringRedisTemplate.boundValueOps("name");
            nameValueOpretions.set("hang");
            nameValueOpretions.append("Mo");
            String s1 = nameValueOpretions.get();
            System.out.println(s1);
    
            //对List Set ZSet Hash进行绑定
            BoundListOperations<String, String> listOperations = stringRedisTemplate.boundListOps("lists");
            listOperations.leftPushAll("mo", "hang", "yuan");
            List<String> lists = listOperations.range(0, -1);
            lists.forEach(list-> System.out.println("list = " + list));
    
            //Set
            // redisTemplate.boundSetOps()
            // stringRedisTemplate.boundSetOps();
            //ZSet
            // redisTemplate.boundZSetOps()
            // stringRedisTemplate.boundZSetOps();
            //Hash
            // redisTemplate.boundHashOps()
            // stringRedisTemplate.boundHashOps();
        }
    }
    
  • Redis应用场景

    • 所有具有实效性的功能都可以由Redis中字符串的超时策略完成
      • 如项目中手机验证码存储的实现
      • 如确认订单信息时的倒计时(失效性业务功能)
    • 利用Redis完成分布式系统或集群系统中的Session共享
      • memcache,也是内存型,但数据存储有上限,数据类型比较简单,所以使用不多
      • Redis,内存型,数据存储无上限,数据类型丰富
    • 利用Redis中ZSet可排序set类型
      • 利用分数(如把商品销量作为分数),实现排行榜类功能
    • 利用Redis完成分布式缓存
      • 缓存要求:在内存中,读写快
    • 利用Redis存储一些认证之后的token信息
      • 也是利用Redis超时策略,如微信公共号、小程序,他们的唯一标识不是Id,而是openid,然后利用openid生成令牌(token),保证安全性,token是有时效性的
    • 利用Redis解决分布式集群系统中分布式锁的问题
      • JVM中的synchronize,但是只能解决一个JVM上的多线程并发时的安全问题,没办法解决多线程在分布式系统中的多线程并发问题
      • 利用官方的LRA脚本处理Redis分布式锁
Redis中分布式缓存实现
  • 缓存:Cache

    • 计算机内存中的一段数据
    • 内存中的数据读写快、断电丢失
  • 使用缓存:

    • 提高网站吞吐量,提高网站运行效率
    • 核心解决问题:减轻数据库压力
  • 使用缓存时,一定是数据库中数据极少发生修改,更多用于查询

  • 本地缓存(local cache)

    • 存在于应用服务器内存中的数据;存在于tomcat
  • 分布式缓存(distribute cache)

    • 存储在当前应用服务器内存之外的数据;存在于Redis
  • 集群:将同一个服务的多个节点放在一起共同对系统提供服务的过程

  • 分布式:有多个不同服务集群共同对系统提供服务,这个系统称之为分布式系统(distribute system)

  • 利用Mybatis自身本地缓存,结合Redis实现分布式缓存

    • Mybatis中应用级缓存(二级缓存),SqlSessionFactory级别缓存,所有会话共享

    • 开启二级缓存:

      mapper.xml
      添加标签<cache />			本地缓存
      
    • 查看Cache标签缓存实现

      • Mybatis底层默认使用的是org.apache.ibatis.cache.impl.PerpetualCache实现
    • 使用自定的的Cache类接口实现Mybatis的Cache接口,并对里面的方法进行实现

      //自定义Redis缓存实现
      public class RedisCache implements Cache {
      
      }
      
    • 使用自定义RedisCache实现

      <cache type="com.mo.cache.RedisCache">
      
  • 代码实现

    • 通过applicationContextUtils工具类获取redisTemplate

      //applicationContextUtils
      
      //用来获取Springboot创建好的工厂
      //@Configuration
      @Component
      public class ApplicationContextUtils implements ApplicationContextAware {   //实现ApplicationContextAware,意味告诉SpringBoot这个类需要工厂
          //保留下来的工厂
          private static ApplicationContext applicationContext;
      
          //将创建好的工厂以参数的形式传递给这个类
          @Override
          public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
              this.applicationContext = applicationContext;
          }
      
          //提供在工厂中获取对象的方法     根据id获取对象
          public static Object getBean(String beanName){
              return applicationContext.getBean(beanName);
          }
      
      }
      
    • 自定义RedisCache

      //自定义Redis缓存实现
      public class RedisCache implements Cache {
      
          //通过构造方法给id赋值,id为当前放入缓存的mapper的namespace
          private final String id;
      
          //必须存在构造方法
          public RedisCache(String id) {
              System.out.println("id = "+id);
              this.id = id;
          }
      
          //返回Cache的唯一标识
          @Override
          public String getId() {
              return this.id;     //对于不同的namespace返回不同的id
          }
      
          //往缓存中存放数据      使用RedisTemplate把数据放入缓存中,Value为Object类型的;根据最大的工厂ApplicationContext获取RedisTemplate
          @Override
          public void putObject(Object key, Object value) {
              System.out.println("key = "+ key.toString());
              System.out.println("value = " + value);
              //通过applicationContextUtils工具类获取redisTemplate
              RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
              redisTemplate.setKeySerializer(new StringRedisSerializer());    //指定放入时key的序列化规则
              redisTemplate.setHashKeySerializer(new StringRedisSerializer());    //指定放入HashKey的序列化规则
      
              //使用RedisHash类型作为缓存存储类型
              //redisTemplate.opsForHash().put(id.toString(), key.toString(), value);
              //使用MD5优化key
              redisTemplate.opsForHash().put(id.toString(), getKeyToMD5(key.toString()), value);
          }
      
          //从缓存中获取数据
          @Override
          public Object getObject(Object key) {
              System.out.println("key = " + key);
              //通过applicationContextUtils工具类获取redisTemplate
              RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              redisTemplate.setHashKeySerializer(new StringRedisSerializer());
      
              //根据key从redis的hash类型中获取数据
              //return redisTemplate.opsForHash().get(id.toString(), key.toString());
              //相对应的取数据时使用MD5取
              return redisTemplate.opsForHash().get(id.toString(),getKeyToMD5(key.toString()));
          }
      
          //这个方法为mybatis保留方法,默认没有实现,后续版本可能会实现
          @Override
          public Object removeObject(Object o) {
              System.out.println("根据指定的key删除缓存");
              return null;
          }
      
          @Override
          public void clear() {
              //发生增删改的任意方法时,都会调用clear方法
              System.out.println("清空缓存");
      
              //清空namespace
              getRedisTemplate().delete(id.toString());    //清空缓存
          }
      
          //用来计算缓存的数量(击中率)
          @Override
          public int getSize() {
              RedisTemplate redisTemplate = getRedisTemplate();
              //获取hash中的key-value数量
              return redisTemplate.opsForHash().size(id.toString()).intValue();   //Redis默认返回值为lang类型,需要转换为int
          }
      
          //封装redisTemplate,解决代码冗余
          private RedisTemplate getRedisTemplate(){
              RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              redisTemplate.setHashKeySerializer(new StringRedisSerializer());
              return redisTemplate;
          }
        
          //优化:封装一个对key进行MD5处理的方法
          private String getKeyToMD5(String key){
              return DigestUtils.md5DigestAsHex(key.getBytes());
          }
      }
      
    • 使用自定义Cache

      <!--开启mybaits的二级缓存-->
      <cache type="com.mo.cache.RedisCache"/>
      
  • 解决关联关系其中一方更新时缓存信息的问题

    //将多个具有关联关系的查询缓存放到一起处理
    <cache-ref namespace="com.mo.dao.UserDao"/>		//namespace:将缓存信息放到UserDao中的缓存中
    
Redis分布式缓存的优化
  • 对放入redis中key进行优化:key的长度不能太长,尽可能的将key设计简洁一些

    • 算法:MD5处理,本身用来加密,也可以对一些文件或字符串进行处理

    • 特点1:文件字符串等经过MD5处理后,都会生成32位16进制的字符串

    • 特点2:不同内容的文件经过MD5加密,结果一定不一致(可用来判断两个文件内容是否一样)

    • 特点3:相同内容的的文件每次经过MD5加密结果一致

    • 在Redis整合Mybatis过程中建议将key进行MD5优化处理

      public void testMD5(){
              String key = "1283276835:3513498832:com.mo.dao.EmpDao.findAll:0:2147483647:select id,username,mobile,did from ems_emp:SqlSessionFactoryBean";
      
              //利用Spring框架提供的MD5工具类
              String s = DigestUtils.md5DigestAsHex(key.getBytes());//直接转成16进制的字符串
              System.out.println(s);
      }
      
相关面试
  • 缓存穿透(击穿)

    • 客户端查询了一个数据库没有的数据记录,导致缓存在这种情况下无法利用
    • Mybatis中Cache解决了缓存穿透:将数据库中没有查询到的结果也进行缓存
  • 缓存雪崩

    • 在系统运行的某一时刻,突然系统中的缓存全部失效;且此时涌来的大量客户端请求,导致所以模块缓存无法利用,大量的请求涌向数据库,导致极端情况数据库阻塞或挂起

    • 缓存存储时,因为业务系统非常大,模块多且业务系统不同,所以放入缓存时针对不同模块都会有一个缓存实效性(超时时间);而这个超时时间就是导致雪崩的最大原因

      //指定不同业务模块设置不同缓存超时时间
      if(id.equals("com.mo.dao.UserDao")){
        	//针对UserDao模块设置超时时间为一小时
        	getRedisTemplate().expire(id.toString(),1,TimeUnit.HOURS);
      }
      
    • 解决方案1:缓存永久存储,不推荐,Redis本身是内存型的数据库,会极大占用服务器内存

    • 解决方案2:针对于不同业务数据一定要设置不同的超时时间

Redis主从复制
  • 主从复制架构中仅用来解决数据的冗余备份,从节点仅用来同步数据

  • 搭建主从复制

    master:
    	port 7000
    	bind 0.0.0.0		//开启远程连接
    
    slave1:
    	port 7001
    	bind 0.0.0.0
    	//默认
    	# slaveof <masterip> <masterport>
    	slaveof 10.211.55.18 7000
    	
    slave2:
    	port 7002
    	bind 0.0.0.0
    	//默认
    	# slaveof <masterip> <masterport>
    	slaveof masterip masterport
    
  • 启动测试

    ./redis-server /root/master/redis.conf 
    ./redis-server /root/slave1/redis.conf 
    ./redis-server /root/slave2/redis.conf 
    
  • 从节点不负责外部请求的处理,只负责同步主节点数据;即默认slave不能进行写操作,只能进行度操作

  • 可以在配置文件redis.conf中修改,使从节点可以进行写操作,但通常不修改

    # Since Redis 2.6 by default slaves are read-only.
    
    //默认
    slave-read-only yes
    
  • 无法解决master节点出现故障时,自动转移功能

Redis哨兵机制
  • Sentinel(哨兵)是Redis的高可用性解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。简单的说哨兵就是带有自动故障转移功能的主从架构

  • 哨兵架构原理

  • 搭建哨兵架构

    //在主节点上创建哨兵配置
    在Master对应的redis.conf同目录下新建sentinel.conf文件,名字绝对不能错
    
    //配置哨兵,在sentinel.conf文件中填入内容
    sentinel monitor 被监视数据库名字(自己起名字) ip port 1
    sentinel monitor master1 10.211.55.18 7000 1	//1代表哨兵数量
    
  • 启动哨兵模式进行测试:第一次启动时,哨兵会自动生成部分配置到redis-sentinel,其中有端口号、启动哨兵脚本所在位置、哪个是master哪个是slave;多个哨兵时需要修改端口号

    //cd /root/redis-4.0.11/src/
    //需要在源码中拷贝redis-sentinel到redis/bin/
    cp redis-sentinel /usr/redis/bin/
    
    //运行,默认端口为26379
    ./redis-sentinel /root/sentinel/sentinel.conf
    数字2是指当有两个及以上的sentinel服务检测到master宕机,才会去执行主从切换的功能
    多个哨兵可以避免脑裂(两个master)的情况发生
    默认哨兵有个心跳超时,指在master出问题后,会在一段时间后才选择slave变为master,并且把原来的master再连接后变为slave
    
    • 自动补全后到sentinel.conf

      sentinel myid 9a91b400a0d632131bf6d2970e89e94482d2bbd8
      # Generated by CONFIG REWRITE
      port 26379	//默认端口号
      bind 0.0.0.0		//开启远程连接权限
      dir "/usr/redis/bin"
      maxmemory 3gb
      sentinel deny-scripts-reconfig yes
      sentinel monitor master1 10.211.55.18 7000 1
      sentinel config-epoch master1 0
      sentinel leader-epoch master1 0
      sentinel known-slave master1 10.211.55.18 7002
      sentinel known-slave master1 10.211.55.18 7001
      sentinel current-epoch 0
      
  • SpringBoot操作Redis哨兵集群

    //application.properties
    #Redis单节点
    #spring.redis.host=10.211.55.18
    #spring.redis.port=6379
    #spring.redis.database=0
    
    #Redis sentinel配置
    #master书写的是使用哨兵监听的哪个名称,为配置文件中sentinel monitor自定义的那个名字,需要对应
    spring.redis.sentinel.master=master1
    #后续哪个节点为主节点哨兵最清楚,故书写哨兵节点,然后根据哨兵节点找到当前这一组master的主节点
    #连接的不再是具体的Redis主机,书写的是多个哨兵节点,多个哨兵之间用逗号隔开
    spring.redis.sentinel.nodes=10.211.55.18:26379
    
  • 无法解决单节点并发问题

  • 无法解决单节点内存和物理磁盘上限

Redis集群
  • Redis在3.0后开始支持Cluster(集群)模式,目前Redis的集群支持节点自动发现,支持slave-master选举和容错,支持在线分片(sharding shard)等特性

  • Redis集群详情

    • Redis中内部的通信用的是心跳机制,Redis进行了再次优化,称为PING-PONG机制,内部封装了二进制协议
    • 所有的Redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化了传输速度和带宽
    • 节点的fail是通过集群中超过半数的节点检测失效时才生效,建议搭建个数为奇数个
    • 客户端与Redis节点直连,不需要中间proxy层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
    • redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster负责维护node<–>slot<–>value
      • Slot槽(Hash槽),一共16384个,近似于均分给集群中所有的master节点;Redis集群最大物理节点上限数为16384个
      • 客户端client的请求会先经过CRC16算法,才可以到达节点处;
      • CRC16算法特点一:对集群模式下所有key进行CRC16计算,计算的结果始终在0-16383之间
      • CRC16算法特点二:对客户端的key进行CRC16计算时,同一个key多次计算结果始终一致
      • CRC16算法特点三:对客户的不同的key进行CRC16计算,计算的key会出现不同的key结果一致
      • reshard重新分配hash槽(重新分片):在集群中动态的添加物理节点时,会重新分配
  • Redis集群搭建

    • 判断一个是集群中的节点是否可用,是集群中的所用主节点选举过程,如果半数以上的节点认为当前节点挂掉,那么当前节点就挂掉了,所有通常搭建Redis集群时建议节点数最好为奇数,搭建集群至少需要三个主节点,三个从节点,至少需要六个节点

      #准备环境安装ruby以及redis集群依赖
      yum install -y ruby rubygems
      gem install redis
      
    • 一个机器上创建七个目录,端口号分别为7000到7006

    • 每个目录复制一份配置文件

      cp redis-4.0.10/redis.conf 7000/
      cp redis-4.0.10/redis.conf 7001/
      cp redis-4.0.10/redis.conf 7002/
      cp redis-4.0.10/redis.conf 7003/
      cp redis-4.0.10/redis.conf 7004/
      cp redis-4.0.10/redis.conf 7005/
      cp redis-4.0.10/redis.conf 7006/
      
    • 修改不同目录配置文件

      port 6379			//修改端口
      bind 0.0.0.0		//开启远程连接
      cluster-config-file nodes-port.conf		//集群节点配置文件
      cluster-node-timeout 5000		//集群节点超时时间,主节点挂掉后可以更快的选举
      appendonly yes		//开起AOF持久化
      sppendfilename "appendonly-7000.aof"	//修改AOF文件
      cluster-enabled yes			//开启集群模式
      cluster-config-file nodes-7000.conf		//修改集群文件
      
      //快照名字
      defilename dump-7000.rdb
      
      //可选
      daemonize no		//守护进程启动,可以后台运行
      
    • 指定不同目录配置文件启动七个节点

      ./redis-server /root/7000/redis.conf
      ./redis-server /root/7001/redis.conf
      ./redis-server /root/7002/redis.conf
      ./redis-server /root/7003/redis.conf
      ./redis-server /root/7004/redis.conf
      ./redis-server /root/7005/redis.conf
      ./redis-server /root/7006/redis.conf
      
    • 查看进程

      [root@redis ~]# ps aux | grep redis			//redis进程必须以cluster启动才算成功
      
    • 从Redis源码包复制集群操作脚本redis-trib.rb到 /usr/redis/bin/目录

      cp redis-trib.rb /usr/redis/bin/
      
    • 创建集群

      #   --replicas为指定每一个物理节点有几个副本节点(从节点)
      ./redis-trib.rb create --replicas 1 10.211.55.18:7000 10.211.55.18:7001 10.211.55.18:7002 10.211.55.18:7003 10.211.55.18:7004 10.211.55.18:7005
      此时会把前三个作为物理(主)节点,后三个作为副本(从)节点
      
      出现All 16384 slots covered为创建集群成功
      
      # 查看集群状态
      ./redis-trib.rb check 10.211.55.18:7000		//节点为查看集群中的哪一个集群,原始集群中任意节点均可
      
      # 集群节点状态说明
      主节点
      	主节点存在hash slots,且主节点的hash slots没有交叉
      	主节点不能删除
      	一个主节点可以有多个从节点
      	主节点宕机时多个副本之间自动选举主节点
      	
      从节点
      	从节点没有hash slots
      	从节点可以删除
      	从节点不负责数据的写,只负责数据的同步
      
    • 连接

      # 单机连接
      ./redis-cli -p 7000
      
      # 集群连接
      ./redis-cli -p 7000 -c
      连接任意一个集群中的节点均可,都会做CRC16处理,然后重定向
      
    • 添加主节点

      # add-node  新加入节点  原始集群中任意节点
      # 该节点必须以集群模式启动
      # 默认情况下该节点是以master节点形式添加,但是新加入的节点没有hsah槽
      ./redos-trib.rb add-node 10.211.55.18:7006 10.211.55.18:7003
      
    • 添加从节点

      # add-node --slave  新加入节点  集群中任意节点
      # 当添加副本节点时没有指定主节点,redis会随机给副本节点较少的主节点添加当前副本节点
      ./redis-trib.rb add-node --slave 10.211.55.18:7006 10.211.55.18:7002
      
      # 为确定的master节点添加主节点
      # add-node --slave --master-id master节点id  新加入节点  集群中任意节点
      ./redis-trib.rb add-node --slave --master-id 3c3sfewassdfsege3e265rtg56as 127.0.0.1:7006  127.0.0.1:7000
      
    • 删除副本节点

      # del-node  集群中任意节点  删除节点id
      # 被删除的节点必须是从节点或是没有分配hash slots的节点
      ./redis-trib.rb del-node 127.0.0.1:7002 3c3sfewassdfsege3e265rtg56as
      
    • 集群在线分片

      # reshard  集群中任意节点  无
      ./redis-trib reshard 10.211.55.18:7000
      
  • java操作Redis cluster集群,建议书写集群中所有节点

    # application.properties
    spring.redis.cluster.nodes=10.211.55.18:7000 , 10.211.55.18:7001 , 10.211.55.18:7002
    
Redis实现分布式Session管理
  • Memcached管理机制

    • 应用服务器和Memcached之间的整合(全局整合):基于tomcat整合Memcached
      • tomcat lib目录下引入Memcached整合jar
      • tomcat conf中的context.xml中配置tomcat整合Memcached
      • 此时的session交由Memcached处理
    • 针对于MSM(memcached session manager),是通过Memcached整合tomcat应用服务,将应用服务中所有部署应用的session全部交给Memcached进行管理;Memcached与Tomcat兼容性差
  • Redis管理机制:必须是基于Spring开发的应用;基于应用方式session管理

    • Redis的session管理是利用Spring提供的Session管理解决方案,将一个应用Session交给Redis存储,整个应用中所有session的请求都会去Redis中获取对应的Session数据
  • RSM(Redis Session Manager):基于某个应用的整合,不需要修改服务器的相关配置

  • Redis完成Session共享

    • Session数据存储到Redis中
  • 开发Session管理

    • 引入依赖

      <dependency>
              <groupId>org.springframework.session</groupId>
              <artifactId>spring-session-data-redis</artifactId>
      </dependency>
      
    • 开发Session管理配置类

      @Configuration
      @EnableRedisHttpSession
      public class RedisSessionManager{
      
      }
      
    • 打包测试即可

  • 实例

    //RedisSessionManager.java
    //配置相关和创建多个组建对象建议@Configuration
    @Configuration
    @EnableRedisHttpSession     //将整个应用中使用session的数据全部交给redis处理
    public class RedisSessionManager {
    
    }
    
    //TestController.java
    @Controller
    @RequestMapping("test")
    public class TestController {
    
        @RequestMapping("test")
        public void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
    
            List<String> list = (List<String>) request.getSession().getAttribute("list");
            if(list == null){
                list = new ArrayList<>();
            }
            list.add("mo");
    
            //Redis管理Session后,每次Session变化都要同步Session
            request.getSession().setAttribute("list",list);     //每次JVM中数据改变,都要再次放入Redis中,使数据同步
            response.getWriter().println("size:" + list.size());
            response.getWriter().println("sessionId"+request.getSession().getId());
    
        }
    
        @RequestMapping("logout")
        public void logout(HttpServletRequest request){
            //退出登录
            request.getSession().invalidate();      //失效,会自动把Redis中的Session清除
        }
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值