Redis之吐血总结

652 阅读15分钟

Redis是什么?

NoSQL是什么?

在介绍Redis之前,我们先简单聊一下什么是NoSQL,NoSQL是基于内存的数据库,特指非关系型数据库,具备高并发、高可用、大数据存储等特点。 目前主流的NoSQL有如下:

  • 键值对存储数据库 - redis
  • 文档型数据库 - MongoDB
  • 列存储数据库 - Hbase
  • 图形数据库

Redis概述

Redis是其中一种非关系型数据库(NoSQL),作为高性能的key-value数据库,支持每秒十几万的读/写操作,其性能超数据库,并且支持集群、分布式、主从同步等配置,还支持一定事务能力。

常用场景

  • 分布式锁
  • 热点数据缓存
  • 消息队列
  • 发布订阅功能
  • 分布式Session共享

Redis的数据类型

String类型

常用命令

// 赋值
set key value;
// 获取数据
get key;
// 赋值一个值,同时返回值    
getset key value;
// 当值不存在时,赋值(可实现分布式锁)   
setnx key value;
    0:key存在,赋值失败
    1:key不存在,赋值成功
    
    
(针对key为int类型使用)    
// 每次对key的值递增1     
incr key;
// 每次对key递增i的值    
incr key i;
// 每次对key的值递减1    
decr key;
// 每次对key的值递减i
decr key i;


// 向key拼接"hello"字符串,并返回长度    
append key "hello";
// 获取key的长度         
strlen key;

为什么 String 类型内存开销大?

String类型的数据存储包含
1.len:数据长度
2.alloc:内存大小
3.buff:数据本身 image.png

RedisObject数据:
RedisObject存储了额外的元数据,记录key的信息
比如最后一次访问的时间、被引用的次数等

指针信息:
redis实际是使用了哈希槽存储数据,每一个哈希槽都对应一个dictEntry。
dictEntry又包含了以下内容:
1.key的指针地址
2.value的指针地址
3.下一个dictEntry的指针地址

image.png

用什么数据结构可以节省内存?

Redis底层有一种数据结构为压缩列表(ziplist),使用一系列的entry连续存储,
不需要额外的指针来指向下一元素,节省内存空间, 

Redis基于压缩列表实现了list/hash/sortSet等数据结构。 
一个key可以存储整个集合的数据,只需要一个dictEntry, 
而String类型的一个key就需要对应一个dictEntry 
相比较,压缩列表极大的减少了dictEntyr的开销。

image.png

使用场景之分布式ID生成器

当一个表水平分表后,同一张表的数据存储在不同的数据库实例中,此时若采用数据库的自增id,会导致id重复问题。此时可以采用Redis来作为分布式ID生成器。

使用场景之对象缓存

当一个对象需要存储到Redis中,可以将对象序列化成Json数据后,以String格式保存到缓存中。下面是我所写RedisUtils的伪代码

/**
 * 存储对象
 * @param key           key值,需要构建前缀,以区分业务
 * @param value         需要存储的对象
 * @param expireSec     缓存失效时间
 */
public <T> String set(String key, T value, Integer expireSec) {
    if (StringUtils.isBlank(key)) {
        return null;
    }
    String res;
    try (Jedis jedis = jedisResourcePool.getResource()) {
        res = jedis.set(buildKey(key), JsonUtils.toJson(value), SetParams.setParams().ex(expireSec).nx());
    }
    return res;
}

/**
 * 获取对象
 * @param key           key值,需要构建前缀,以区分业务
 * @param clazz         获取的对象类型,用于反序列化
 */
public <T> T get(String key, Class<T> clazz) {
    if (StringUtils.isBlank(key)) {
        return null;
    }

    try (Jedis jedis = jedisResourcePool.getResource()) {
        String result = jedis.get(buildKey(key));
        // 反序列化对象后返回
        return JsonUtils.fromJson(result, clazz);
    } catch (Exception e) {
        LOGGER.info("获取缓存失败,key={}", key, e);
        return null;
    }
}

使用场景之分布式锁

分布式锁的实现方式

  • 基于redis的分布式锁
  • 基于zookeeper的分布式锁
  • 基于数据库的悲观锁

设置分布式锁的注意事项

  • 同一性:获取锁和释放锁必须是同一个请求
  • 时效性:获取锁之后,必须设置过期时间,防止死锁的情况
  • 互斥性:一个锁只能被一个客户端持有

采用Redis实现的分布式锁

  • set方式
public boolean redisLock(String lockKey, String requestValue, int expire) {
    String result = jedis.set(lockKey,requestValue,"NX","EX",int expire)
    if(result.equals("ok")){
        // 获取分布式锁成功
        return true;
    }
    return false;
}

/**
 * 获取分布式锁的参数说明
 * @param lockKey       用于获取锁的业务key,例如订单id
 * @param requestValue  请求值
 * @param NX|XX 	NX-仅在键不存在时设置键。XX-只有在键已存在时才设置。
 * @param EX|PX 	设置指定的到期时间(EX以秒为单位,PX以毫秒为单位)。
 * @param expire	过期时间
 */
 jedis.set(String lockKey,String requestValue,String NX|XX,String EX|PX,int expire);

  • setnx方式
public boolean redisLock(String lockKey, String requestValue, int expireTime) {
    Long result = jedis.setnx(lockKey,requestValue);
    // 获取锁成功
    if(result == 1){
    
        // 此处若发生异常,则导致加过期时间失败,可能会导致死锁
        
        // 设置过期时间
        jedis.expire(lockKey,expireTime);
        return true;
    }
    return false;
}
  • 释放锁
public void releaseLock(String lockKey,String requestValue){
    //保证获取锁和释放锁必须是同一个请求
    if(jedis.get(lockKey).equals(requestValue)){  
        jedis.del(lockKey);   //释放锁
    }
} 

List类型

redis中的list采用的是LinkList双向链表,越接近两端,则获取值速度越快。可用于消息队列的实现。

常用命令

// 想链表左边添加元素
lpush list v1,v2:
// 向链表右边添加元素    
rpush list v1,v2:
// 删除最左边的元素 
lpop list:
// 删除最右边的元素
rpop list:
// 获取列表中两索引之间的值,当start等于0,end等于-1时,获取所有的值
lrange list start end:
// 获取索引为i的值。
lindex list i:
// 获取列表的长度。
llen list:

使用场景之消息队列

list类型的lpush和rpop(或者反过来lpop和rpush)能实现队列的先进先出功能,所以Redis的list类型能够实现简单的点对点消息队列。不过主流还是使用RocketMQ、Kafka、RabbitMQ等成熟的消息队列。

Hash散列类型

为键值对的数据类型,value只能为String类型

image.png

常用命令

// 例如给user的username赋值zhanshan
hset user username zhansan;
// 给map多个key进行赋值
hmset map key1 value1 key2 value2;
// 当key不存在时,才对key进行赋值
hsetnx map key1 value1;

// 获取map的key值
hget map key;
// 获取多个key的值
hmget map key1 key2;
//获取所有键值对
hgetAll map;
// 删除map中的key值
hdel map key;

常用场景

  • 存储一些对象的属性,特别是容易发生增删改查的数据
  • 例如:商品属性(如商品库存)

Set数据类型

set集合类型,它具备无序性/去重性

常用命令

// 给集合set添加多个元素
sadd set member1 member2 member3;
// 获取集合set的所有元素
smembers set;
// 删除元素
sremove set member1;
// 获取元素个数
scard set;
// 判断元素1是否存在
sismember set member1;

set集合运算

  • 差集运算

image.png

//返回A-B的差集
SDiff setA setB;  
  • 并集运算

image.png

// 返回AB的并集
sUnion setA setB;  
  • 交集运算

image.png

// 返回AB的交集部分
sInner setA setB;

ZSet有序集合

有序集合,在set的基础之上关联了分数进行排序

相关命令

// 添加元素1以及对应分数
zadd zset score1 member1;
// 从小到大,获取索引之间的元素
zrange zset startIndex endIndex;
// 从大到小,获取索引之间的元素
zrevrange zset startIndex endIndex;
// 删除元素1
zrem zset member1;
// 获取分数1到分数2之中内的元素
zrangeByScore zset score1 score2;
// 删除指定分数内的元素
zremrangeByScore zset score1 score2;

常用场景

  • 商品销量排行
  • 点赞数排行

Redis 事务

  • 介绍

Redis事务是为了保证一组命令的原子性(基于队列),主要通过四个命令实现

  • 相关命令

multi;标记事务的开始,后续执行的redis命令会存入队列之中.
exec; 执行事务中的命令集队列.
discard; 清楚事务中的命令集队列
watch; 用于监控某个key的变化情况,当key发送变化时,事务不执行(可实现乐观锁)

  1. watch key : 监控某个key
  2. unwatch key:不监控某个key
  • 事务失败机制
redis事务不支持回滚,若队列中的某个命令发生了错误,会分为两种情况处理:   
情况1:
redis语法错误,此时整个事务都不执行.
情况2:
redis语法正确,但是由于类型错误的命令而执行失败,此时事务会执行正确的命令,而忽略错误的命令

不支持事务回滚的原因:
因语法错误和类型错误都是在开发阶段可以预见以及避免的,因此redis为了提高性能,不支持事务的回滚

redis持久化方案

在满足特定条件时,redis将缓存数据写入到磁盘中。分为rdb方式(默认)和aof方式

rdb快照方式

rdb是redis默认的持久化方式,rdb快照时,先将数据写入到临时,会将主机的数据写入到rdb文件中。
当redis宕机后重启时,会读取rdb文件中的数据进行恢复到缓存中。

  • 快照触发的时机

    • 触发配置文件中的条件-- redis.config文件

    例 save 60 5 : 60秒内,有5个key发生了变化,则进行快照

    • 执行save命令

    save命令会阻塞redis服务器的进程,并进行快照,期间redis不能处理任何命令,直到rdb文件生成完毕

    • 执行bgsave命令(主从同步)

    bgsave命令不会阻塞redis服务器的线程,当bgsave命令执行时,redis父线程会fork一个子线程进行快照,而父线程继续处理服务器的请求,不会造成阻塞,因此bgsave更适用于线上数据的操作

  • bgsave不阻塞的原理

当redis进行快照时,redis的会fork一个子进程进行快照,将缓存中的数据写入到rdb文件中,
而父进程则继续处理客户端的请求,这种方式提高了redis的性能,在进行快照时,不会阻塞到其它请求。
当rdb文件写入完成后,才会将旧的rdb文件替换掉,保证rdb文件的完整性。
注:rdb文件是一种二进制文件,占用空间小,传输更快。 image.png

  • 快照方式的优缺点

    • 优点

    rdb快照方式相较于aof持久化方式,对redis的性能损耗较小,它只有在满足特定条件下,才会执行持久化。同时它是通过fork子进程的方式进行快照,而父进程可以继续处理客户端的请求。

    • 缺点

    如果redis宕机的话,会丢失最后一次的正在进行的快照信息,当数据量比较大时,fork时间过长,会导致客户端请求的阻塞。

aof方式

aof方式是redis优化重写的持久化方案,当客户端每次请求服务器时,都会将每次更改的内容写入到aof日志中。
就算redis宕机,aof文件也包含了所有数据,因此它保证了数据的安全性,但它对redis的性能损耗较高。

  • aof的开启方式 aof默认是不开启的,如果需要开启,则需要修改redis.conf文件

    appendonly yes :表示开启aof持久化方式,默认是appendonly no
    appendfilename "appendonly.aof" :指定写入aof的文件名

  • aof的重写优化原理

    • 安全原理

    当客户端每次请求redis写操作时,redis每执行一次写命令,在对新的aof文件写入时,同时会对旧的aof同步写入,保证了写入过程中,redis宕机,也不会丢失旧数据。
    当新的aof文件写入完毕后,新的aof文件包含了恢复当前数据的最小命令集合,才会替代旧的aof文件,内存相较于旧的aof文件,内存更小。

    • 特点

    当aof文件过大时,会自动进行优化重写

    • 优化重写的配置

    auto-aof-rewrite-percentage 80 : 表示aof文件超过上一个aof文件的百分比,则开始重写
    auto-aof-rewrite-min-size-64mb : 表示aof文件需要优化重写的最小内存

  • 图解aof持久化过程

image.png

  • aof相关参数调优

appendfsync always //每次写入磁盘都同步到aof文件中,保证数据安全,但影响性能
appendfsync everysec //每秒执行一次磁盘和aof文件的同步
appendfsync no //不主动同步,交由操作系统处理,速度快,但此种方法不安全

  • aof文件修复

    • 场景:

    当redis在重写优化时,发生了宕机,则再次重启时,会拒绝加载此aof文件,保证了数据一致性,并对其修复。

    • 修复方式:

    1、备份现用的aof文件
    2、启动redis-check-aof命令,对原有的aof文件进行修复
    3、重启redis服务,等待服务器载入修复后的aof文件,进行数据的修复。

持久化方案比较

rdb方式更适用于对数据安全不高,此方案性能更优 aof方式适用于对数据安全要求高的场景,此方案性能更安全

redis主从复制

持久化方案保证了redis宕机后,可以从硬盘恢复数据。当单点硬盘故障后,持久化方案不再
可以恢复数据,此时需通过主从复制来进行恢复redis服务。

  • 主从复制机制

image.png

特点

1、redis主机每更新完数据候,会实时同步给从机。
2、当主机宕机后,从机可以继续提供服务
3、只能有一台主机,但可以有多台从机
4、从机可以提供读操作,但不可提供写操作。

  • 主从同步配置

主机:

无需配置
从机:
修改redis的conf文件
slaveof 192.168.0.1 6379 //主机地址

  • 同步过程

slave第一次连接时(全量同步):

1、slave向master发送sync命令
2、master接收完sync命令后,执行bgsave命令,生成快照文件
3、slave读取快照文件实现全量同步。
增量同步:
当slave初始化完毕后,master每更新一次数据,则将命令同步发送给slave。

image.png

增量同步,主库和从库在第一次进行全量同步之后,会建立一个长连接,主库会陆续将接受到的
写请求,发送给从库,避免了频繁的网络连接. 

长连接带来的问题: 
当主库和从库存在网络波动,此时写请求无法及时发送给从库. 

redis2.8之前: 
    如果出现网络断开,则主库会重新给从库进行一次全量同步 
    
redis2.8之后: 
    主库会将网络断开后的这段时间产生的写请求写入到缓冲区中,待网络恢复正常后, 
    再将缓冲区的写请求发送给从库,实现增量同步 
    
    缓冲环大小设置: 
      repl_backlog_size 
    如果它配置得过小,在增量复制阶段,可能会导致从库的复制进度赶不上主库, 
    进而导致从库重新进行全量复制。

image.png

redis哨兵机制

  • 作用

监控:

哨兵可以通过心跳机制,监控redis集群中的各个节点的健康情况 故障切换: 当哨兵监控到master节点故障后,会通过选举机制将一个slave从机切换为master主机 提醒: 当哨兵监控到某个节点故障后,可以通知管理员或者其它应用

  • 故障切换的过程

1、首先哨兵会将slave转为master,并将其它slave指向新的master
2、当客户端连接redis集群时,redis会给客户端返回新master的ip地址

  • 哨兵的工作过程

1、哨兵会定期地向集群的每个节点发送ping命令
2、当集群中的服务器超过一定时间没有响应pong命令,则被哨兵标记为主观下线。
3、当一个服务被标记为主观下线后,所有的哨兵都会向该服务发送ping命令

3.1、当超过一定数量的哨兵没有接收到服务的pong时,则服务会被标记为客观下线
3.2、当没有足够多的哨兵同意服务客观下线时,则移除掉主观下线

  • 图解 image.png

redis缓存问题

缓存数据的过程(被动缓存):

1、先查询缓存,当缓存无数据则查询数据库。 2、查询数据库,当数据库有数据则写入缓存中。

  • 过期key的删除策略
    redis默认提供了两种对过期key的删除策略

    • 惰性删除: 当客户端访问到过期的key时,才对过期key进行删除

    优点:对cpu友好,不会占用太多cpu
    缺点:无效的过期key占用过多,影响redis的内存大小。

    • 定时删除: redis每个一段时间,就会扫描过期的key,为了不影响性能,每次只会扫描部分的key将其删除,减少对cpu的影响。

    优点:增加空间利用率。
    缺点:redis需要频繁扫描过期key,并操作删除,影响cpu性能。

基于以上原因,redis默认采用惰性删除+定时删除的混合策略。

  • 缓存穿透

    • 概念

    当缓存和数据库都无key值时,大量并发请求该key时,造成服务器压力大,则是缓存穿透

    • 解决方法

    1、当数据库没该key值时,也对其进行缓存,当进行insert操作时,再更新该缓存。
    2、布隆过滤器(bitmap位图),向其存储数据库不存在的key,请求都经过布隆器过滤掉无效请求

  • 缓存击穿

    • 概念

    指缓存由于到期时间没有该数据,而数据库有该数据,此时有高并发的请求读取缓存,
    而缓存没此数据,则高并发对数据库造成很多压力则是缓存击穿。

    • 解决方法

    1、设置热点数据不过期
    2、采用分布式锁,防止高并发同时请求数据库

  • 缓存雪崩

    • 概念

    当某一时间有大量的key值过期失效,此时有高并发请求这些key,造成数据库压力大,则叫缓存雪崩。

    • 解决方法

    1、不同的key设置不同的过期时间。
    2、二级缓存
    2、缓存失效后,加锁或使用队列来控制查询数据库和写缓存的线程数