目录

一、概念

1、什么是nosql

2、什么是redis

3、redis特点

4、redis优势

5、redis安装

6、redis可视化工具

7、应用场景

8、Java简单工具类

9、项目启动对应redis清空

二、数据类型

String(字符串)

应用场景

List(列表)

应用场景

Set(集合)

应用场景

sorted set(有序集合)

应用场景

hash(哈希)

应用场景

三、Redis持久化

1、什么是持久化

2、RDB持久化

3、AOF持久化

优点

缺点

4、RDB与AOF对比​编辑

5、RDB和AOF如何选择

6、Redis的数据恢复

四、事务

数据库的事务

redis事务

五、主从复制

1、主从复制作用

2、复制原理

3、同步策略

六、哨兵与集群

1、redis哨兵

​1、Sentinel的作用

2、Sentinel的工作方式

2、redis集群

七、常见问题

缓存雪崩

缓存击穿

缓存穿透

布隆过滤器

数据一致性

缓存预热

热key问题

大key问题

Redis的过期数据回收策略

Redis常见性能问题和解决方案


一、概念

1、什么是nosql

Nosql = not only sql(不仅仅是SQL)

关系型数据库:列+行,同一个表下数据的结构是一样的。

非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。

NoSQL泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0大数据时代!尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的。

传统RDBMS和NoSQL

RDBMS
 - 组织化结构
 - 固定SQL
 - 数据和关系都存在单独的表中(行列)
 - DML(数据操作语言)、DDL(数据定义语言)等
 - 严格的一致性(ACID): 原子性、一致性、隔离性、持久性
 - 基础的事务
NoSQL
 - 不仅仅是数据
 - 没有固定查询语言
 - 键值对存储(redis)、列存储(HBase)、文档存储(MongoDB)、图形数据库(不是存图形,放的是关系)(Neo4j)
 - 最终一致性(BASE):基本可用、软状态/柔性事务、最终一致性

2、什么是redis

Redis = Remote Dictionary Server,即远程字典服务。

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

与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

  • redis是一个开源的key-value存储系统
  • 和Memcached类似,它支持的value类型相对更多,包括String(字符串)、List(链表)、Hash(哈希)、Set(无需不重复集合)、ZSet(sorted set有一定顺序的集合)
  • 与memecached一样,redis数据都缓存在内存中
  • 区别是redis会周期性的把更新的数据写入到磁盘或者把修改操作追加到记录文件
  • 并且再次基础上实现了master-slaver(主从同步)

3、redis特点

1、Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
2、Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
3、Redis支持数据的备份,即master-slave模式的数据备份。

4、redis优势

1、性能极高 : Redis能读的速度是110000次/s,写的速度是81000次/s 。
2、丰富的数据类型 : Redis支持 String, List, Hash, Set 及 Sorted Set 数据类型操作。
3、原子 : Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
4、丰富的特性 : Redis还支持发布/订阅, 通知, key 过期等特性。

5、redis安装

Linux安装Redis_托尼Lee的博客-CSDN博客一、下载二、安装三、配置启动https://blog.csdn.net/libusi001/article/details/103274142

6、redis可视化工具

https://blog.csdn.net/libusi001/article/details/103267666https://blog.csdn.net/libusi001/article/details/103267666

7、应用场景

这是Redis应用最广泛地方,基本所有的Web应用都会使用Redis作为缓存,来降低数据源压力,提高响应速度。

  • Redis天然支持计数功能,而且计数性能非常好,可以用来记录浏览量、点赞量等等。
  • Redis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。
  • 赞/踩、粉丝、共同好友/喜好、推送、下拉刷新。
  • Redis提供了发布订阅功能和阻塞队列的功能,可以满足一般消息队列功能。
  • 分布式环境下,利用Redis实现分布式锁,也是Redis常见的应用。

Redis的应用一般在项目的使用举例:

  1. 热点数据缓存:首页数据、
  2. Token存储:用户登录成功之后,使用Redis存储Token
  3. 登录失败次数计数:使用Redis计数,登录失败超过一定次数,锁定账号目前设置5次锁定
  4. 分布式锁:分布式环境下登录、注册等操作加分布式锁发送短信
  5. 地址缓存:对省市区数据的缓存

8、Java简单工具类

package com.god.demo;

import com.god.demo.config.redis.RedisUtil;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
class RedisTests {

    private final static Logger log = LoggerFactory.getLogger(RedisTests.class);

    @Resource
    private RedisUtil redisUtils;

    //=============================common============================

    @Test
    public void set() {
        redisUtils.set("kkk", "123");
        log.info("插入缓存数据kkk,{}", "123");
    }

    @Test
    public void isExist() {
        boolean value = redisUtils.hasKey("kkk");
        log.info("是否存在,{}", value);
    }

    @Test
    public void get() {
        log.info("读取缓存数据kkk,{}", redisUtils.get("kkk"));
    }

    @Test
    public void expire() {
        redisUtils.expire("kkk", 200);
        log.info("指定缓存失效时间kkk,{}", "200");
    }

    @Test
    public void getExpire() {
        long kkk = redisUtils.getExpire("kkk");
        log.info("根据key 获取过期时间kkk,{}", kkk);
    }

    @Test
    public void del() {
        redisUtils.del("kkk");
        log.info("删除缓存kkk,{}", "kkk");
    }

    @Test
    public void isExist2() {
        boolean value = redisUtils.hasKey("kkk");
        log.info("是否存在2,{}", value);
    }

    //============================String=============================

    @Test
    public void set2() {
        redisUtils.set("kkk", "123", 10);
        log.info("普通缓存放入并设置时间kkk,{},{}", "123", 10);
    }

    @Test
    public void incr() {
        redisUtils.incr("kkk", 2);
        log.info("递增kkk,{}", 2);
    }

    @Test
    public void get2() {
        log.info("读取缓存数据kkk,{}", redisUtils.get("kkk"));
    }

}

9、项目启动对应redis清空

定义一个类,继承ApplicationRunner并且在头部注入Component就可以了。

/**

 * 应用程序启动的时候,删除session_user的表(这个方法实在应用启动之后立即执行)

 */

@Component

public class MyApplicationRunner implements ApplicationRunner {

    @Resource

    private SessionUserDao sessionUserDao;

    @Autowired

    private StringRedisTemplate stringRedisTemplate;

    @Override

    public void run(ApplicationArguments args) throws Exception {

        sessionUserDao.deleteAll();

        Set<String> keys = stringRedisTemplate.keys("*");//清空redis数据库中所有的键值对

        stringRedisTemplate.delete(keys);

        System.out.println("sessionUser全部删除了");

    }

}

Set<String> keys = stringRedisTemplate.keys("*");//清空redis数据库中所有的键值对

stringRedisTemplate.delete(keys);

上面两句就可以清空redis了

二、数据类型

Redis是一个开源,内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。

由于redis类型大家很熟悉,且网上命令使用介绍很多,下面重点介绍五大基本类型的底层数据结构与应用场景,以便当开发时,可以熟练使用redis。

String(字符串)

1.String类型是redis的最基础的数据结构,也是最经常使用到的类型。
  而且其他的四种类型多多少少都是在字符串类型的基础上构建的,所以String类型是redis的基础。
2.String 类型的值最大能存储 512MB,这里的String类型可以是简单字符串、
  复杂的xml/json的字符串、二进制图像或者音频的字符串、以及可以是数字的字符串

应用场景

1、缓存功能:String字符串是最常用的数据类型,不仅仅是redis,各个语言都是最基本类型,因此,利用redis作为缓存,配合其它数据库作为存储层,利用redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。

2、计数器:许多系统都会使用redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。

3、统计多单位的数量:eg,uid:gongming count:0 根据不同的uid更新count数量。

4、共享用户session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存cookie,这两种方式做有一定弊端,1)每次都重新登录效率低下 2)cookie保存在客户端,有安全隐患。这时可以利用redis将用户的session集中管理,在这种模式只需要保证redis的高可用,每次用户session的更新和获取都可以快速完成。大大提高效率。

List(列表)

1.list类型是用来存储多个有序的字符串的,列表当中的每一个字符看做一个元素
2.一个列表当中可以存储有一个或者多个元素,redis的list支持存储2^32次方-1个元素。
3.redis可以从列表的两端进行插入(pubsh)和弹出(pop)元素,支持读取指定范围的元素集,
  或者读取指定下标的元素等操作。redis列表是一种比较灵活的链表数据结构,它可以充当队列或者栈的角色。
4.redis列表是链表型的数据结构,所以它的元素是有序的,而且列表内的元素是可以重复的。
  意味着它可以根据链表的下标获取指定的元素和某个范围内的元素集。

应用场景

1、消息队列:reids的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。

2、文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。

Set(集合)

1.redis集合(set)类型和list列表类型类似,都可以用来存储多个字符串元素的集合。
2.但是和list不同的是set集合当中不允许重复的元素。而且set集合当中元素是没有顺序的,不存在元素下标。
3.redis的set类型是使用哈希表构造的,因此复杂度是O(1),它支持集合内的增删改查,
  并且支持多个集合间的交集、并集、差集操作。可以利用这些集合操作,解决程序开发过程当中很多数据集合间的问题。

应用场景

1、标签:比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容的用户利用一个标签把他们进行归并。

2、共同好友功能,共同喜好,或者可以引申到二度好友之类的扩展应用。

3、统计网站的独立IP。利用set集合当中元素不唯一性,可以快速实时统计访问网站的独立IP。

数据结构

set的底层结构相对复杂写,使用了intset和hashtable两种数据结构存储,intset可以理解为数组。

sorted set(有序集合)

redis有序集合也是集合类型的一部分,所以它保留了集合中元素不能重复的特性,但是不同的是,有序集合给每个元素多设置了一个分数。

redis有序集合也是集合类型的一部分,所以它保留了集合中元素不能重复的特性,但是不同的是,
有序集合给每个元素多设置了一个分数,利用该分数作为排序的依据。

应用场景

1、 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。

2、用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

hash(哈希)

  Redis hash数据结构 是一个键值对(key-value)集合,它是一个 string 类型的 field 和 value 的映射表,
redis本身就是一个key-value型数据库,因此hash数据结构相当于在value中又套了一层key-value型数据。
所以redis中hash数据结构特别适合存储关系型对象

应用场景

1、由于hash数据类型的key-value的特性,用来存储关系型数据库中表记录,是redis中哈希类型最常用的场景。一条记录作为一个key-value,把每列属性值对应成field-value存储在哈希表当中,然后通过key值来区分表当中的主键。

2、经常被用来存储用户相关信息。优化用户信息的获取,不需要重复从数据库当中读取,提高系统性能。

三、Redis持久化

1、什么是持久化

持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。将Redis所有数据保存在内存中,对数据的更新将异步地保存到磁盘上。

2、RDB持久化

RDB = Redis DataBase,是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:
1、只有一个文件 dump.rdb,方便持久化。
2、容灾性好,一个文件可以保存到安全的磁盘。
3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
4、数据集大的时候,比 AOF 的启动效率高。
缺点:
1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。
三种触发持久化的方式:
a、save触发方式:该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。
b、bgsave触发方式:bgsave 命令执行一个异步操作,以RDB文件的方式保存所有数据的快照。Redis使用Linux系统的fock()生成一个子进程来将DB数据保存到磁盘,主进程继续提供服务以供客户端调用。
c、自动化触发:除了手动执行 save 和 bgsave 命令实现RDB持久化以外,Redis还提供了自动生成RDB的方式。可以通过配置文件对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动进行数据集保存操作。
bgsave命令执行流程:

1、执行bgsave命令,Redis进程先判断当前是否存在正在执行的子线程,如果存在直接
结束。
2、Redis进程执行fork操作创建子线程,在fork操作的过程中Redis进程会被阻塞。
3、Redis进程fork完成后,bgsave命令就结束了,自此Redis进程不会被阻塞,可以响应其他命令。
4、子进程根据Redis进程的内存生成快照文件,并替换原有的RDB文件。
5、子进程通过信号量通知Redis进程已完成。

3、AOF持久化

AOF持久化 = Append Only File持久化,则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

优点

1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点

1、AOF 文件比 RDB 文件大,且恢复速度慢。
2、数据集大的时候,比 rdb 启动效率低。
持久化3种策略:
a、always:每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全
b、everysec:每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。
c、no:从不 fsync :将数据交给操作系统来处理,由操作系统来决定什么时候同步数据。更快,也更不安全的选择。
AOF持久化流程:

1、命令追加(append):所有写命令都会被追加到A0F缓存区(aof buf)中。
2、文件同步(svnc):根据不同策略将AOF缓存区同步到AOF文件中。
3、文件重写(rewrite):定期对AOF文件进行重写,以达到压缩的目的。
4、数据加载(load):当需要恢复数据时,重新执行AOF文件中的命令。

4、RDB与AOF对比

5、RDB和AOF如何选择

  • 一般来说, 如果想达到足以媲美数据库的 数据安全,应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
  • 如果 可以接受数分钟以内的数据丢失,那么可以 只使用 RDB 持久化
  • 有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外,使用 RDB 还可以避免 AOF 程序的 bug。
  • 如果只需要数据在服务器运行的时候存在,也可以不使用任何持久化方式。

6、Redis的数据恢复

当Redis发生了故障,可以从RDB或者AOF中恢复数据。

恢复的过程也很简单,把RDB或者AOF文件拷贝到Redis的数据目录下,如果使用AOF恢复,配置文件开启AOF,然后启动redis-server即可。

Redis 启动时加载数据的流程:

  1. AOF持久化开启且存在AOF文件时,优先加载AOF文件。
  2. AOF关闭或者AOF文件不存在时,加载RDB文件。
  3. 加载AOF/RDB文件成功后,Redis启动成功。
  4. AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

四、事务

事务本质:一组命令的集合

数据库的事务

数据库事务通过ACID(原子性、一致性、隔离性、持久性)来保证。

数据库中除查询操作以外,插入(Insert)、删除(Delete)和更新(Update)这三种操作都会对数据造成影响,因为事务处理能够保证一系列操作可以完全地执行或者完全不执行,因此在一个事务被提交以后,该事务中的任何一条SQL语句在被执行的时候,都会生成一条撤销日志(Undo Log)。

redis事务

redis事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。

Redis中一个事务从开始到执行会经历开始事务(muiti)、命令入队和执行事务(exec)三个阶段,事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。

一组命令中存在两种错误不同处理方式

1.代码语法错误(编译时异常)所有命令都不执行

2.代码逻辑错误(运行时错误),其他命令可以正常执行 (该点不保证事务的原子性)

为什么redis不支持回滚来保证原子性

这种做法的优点:

    Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
    因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

**鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。

事务监控

悲观锁:认为什么时候都会出现问题,无论做什么操作都会加锁。

乐观锁:认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据。

使用cas实现乐观锁

redis使用watch key监控指定数据,相当于加乐观锁

watch保证事务只能在所有被监视键都没有被修改的前提下执行, 如果这个前提不能满足的话,事务就不会被执行。

watch执行流程

五、主从复制

1、主从复制作用

a、读写分离
b、数据容灾

2、复制原理

3、同步策略

主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步

六、哨兵与集群

1、redis哨兵

sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:

  •     集群监控:负责监控 redis master 和 slave 进程是否正常工作。
  •     消息通知:如果某个 redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  •     故障转移:如果 master node 挂掉了,会自动转移到 slave node上。
  •     配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。


1、Sentinel的作用

a、Master 状态监测
b、如果Master 异常,则会进行Master-slave 转换,将其中一个Slave作为Master,将之前的Master作为Slave
c、Master-Slave切换后,master_redis.conf、slave_redis.conf和sentinel.conf的内容都会发生改变,即master_redis.conf中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换

2、Sentinel的工作方式

a、每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。
b、如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。
c、如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
d、当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。
e、在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 。
f、当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 。
g、若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。

2、redis集群

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
为什么是16384(2^14)个?
在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。
虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) * 1024(1k) =65K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。
方案说明:
a、通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
b、每份数据分片会存储在多个互为主从的多节点上
c、数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
d、同一分片多个节点间的数据不保持一致性
e、读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
f、扩容时时需要需要把旧节点的数据迁移一部分到新节点

七、常见问题

缓存雪崩

问题描述: 缓存同一时间大面积的失效,导致所有的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
a、缓存数据过期时间随机:过期时间设置随机,防止同一时间大量数据过期现象发生。
b、热点数据不设置过期时间,主动刷新缓存:缓存设置成永不过期,在更新或删除 DB 中的数据时,也主动地把缓存中的数据更新或删除掉。
c、检查更新:缓存依然保持设置过期时间,每次 get 缓存的时候,都和数据的过期时间和当前时间进行一下对比,当间隔时间小于一个阈值的时候,主动更新缓存。
d、使用锁:通过互斥锁或者队列,控制读数据库和写缓存的线程数量。

缓存击穿

问题描述: 缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,读缓存没读到数据,造成数据库短时间内承受大量请求而崩掉。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是缓存同一时间大面积失效。
解决方案:
1、设置热点数据永远不过期。
2、加互斥锁

缓存穿透

问题描述: 缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
a、接口层增加逻辑校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
b、从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
c、采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

布隆过滤器

布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即0或者1, 来标识数据是否存在。

存储数据的时候,使用K个不同的哈希函数将这个变量映射为bit列表的的K个点,把它们置为1。

我们判断缓存key是否存在,同样,K个哈希函数,映射到bit列表上的K个点,判断是不是1:

  • 如果全不是1,那么key不存在;
  • 如果都是1,也只是表示key可能存在。

布隆过滤器也有一些缺点:

  1. 它在判断元素是否在集合中时是有一定错误几率,因为哈希算法有一定的碰撞的概率。
  2. 不支持删除元素。

数据一致性

数据强一致性方案: 读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况,串行化之后,就会导致系统的吞吐量会大幅度的降低.
还有一种方式就是可能会暂时产生数据不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。

根据CAP理论,在保证可用性和分区容错性的前提下,无法保证一致性,所以缓存和数据库的绝对一致是不可能实现的,只能尽可能保存缓存和数据库的最终一致性。

选择合适的缓存更新策略

1. 删除缓存而不是更新缓存

当一个线程对缓存的key进行写操作的时候,如果其它线程进来读数据库的时候,读到的就是脏数据,产生了数据不一致问题。

相比较而言,删除缓存的速度比更新缓存的速度快很多,所用时间相对也少很多,读脏数据的概率也小很多。

先更数据,后删缓存先更数据库还是先删缓存?这是一个问题。

更新数据,耗时可能在删除缓存的百倍以上。在缓存中不存在对应的key,数据库又没有完成更新的时候,如果有线程进来读取数据,并写入到缓存,那么在更新成功之后,这个key就是一个脏数据。

毫无疑问,先删缓存,再更数据库,缓存中key不存在的时间的时间更长,有更大的概率会产生脏数据。

目前最流行的缓存读写策略cache-aside-pattern就是采用先更数据库,再删缓存的方式。

缓存不一致处理

如果不是并发特别高,对缓存依赖性很强,其实一定程序的不一致是可以接受的。

但是如果对一致性要求比较高,那就得想办法保证缓存和数据库中数据一致。

缓存和数据库数据不一致常见的两种原因:

  • 缓存key删除失败
  • 并发导致写入了脏数据

缓存一致性

消息队列保证key被删除可以引入消息队列,把要删除的key或者删除失败的key丢尽消息队列,利用消息队列的重试机制,重试删除对应的key。

这种方案看起来不错,缺点是对业务代码有一定的侵入性。

数据库订阅+消息队列保证key被删除可以用一个服务(比如阿里的 canal)去监听数据库的binlog,获取需要操作的数据。

然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除操作。这种方式降低了对业务的侵入,但其实整个系统的复杂度是提升的,适合基建完善的大厂。

延时双删防止脏数据还有一种情况,是在缓存不存在的时候,写入了脏数据,这种情况在先删缓存,再更数据库的缓存更新策略下发生的比较多,解决方案是延时双删。

简单说,就是在第一次删除缓存之后,过了一段时间之后,再次删除缓存。

延时双删

这种方式的延时时间设置需要仔细考量和测试。

设置缓存过期时间兜底

这是一个朴素但是有用的办法,给缓存设置一个合理的过期时间,即使发生了缓存数据不一致的问题,它也不会永远不一致下去,缓存过期的时候,自然又会恢复一致。

缓存预热

所谓缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法:

1、直接写个缓存刷新页面或者接口,上线时手动操作

2、数据量不大,可以在项目启动的时候自动进行加载

3、定时任务刷新缓存.

热key问题

问题描述: 突然有几十万的请求去访问redis上的某个特定key。那么,这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机。那接下来这个key的请求,就会直接怼到你的数据库上,导致你的服务不可用。
解决方案:
a、利用二级缓存:在你发现热key以后,把热key加载到系统的JVM中。针对这种热key请求,会直接从jvm中取,而不会走到redis层。
b、备份热key:不要让key走到同一台redis上,我们把这个key在多个redis上都存一份,当有热key请求进来的时候,我们就在有备份的redis上随机选取一台,进行访问取值,返回数据。

大key问题

问题描述: 存储本身的key值空间太大,或者hash,list,set等存储中value值过多。
主要包括:
a、单个简单的key存储的value很大
b、hash, set,zset,list 中存储过多的元素
c、一个集群存储了上亿的key
解决方案:

    单个简单的key存储的value很大
    a、对象需要每次都整存整取:可以尝试将对象分拆成几个key-value, 使用multiGet获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响;
    b、该对象每次只需要存取部分数据:可以像第一种做法一样,分拆成几个key-value, 也可以将这个存储在一个hash中,每个field代表一个具体的属性,使用hget,hmget来获取部分的value,使用hset,hmset来更新部分属性
    hash, set,zset,list 中存储过多的元素
    可以对存储元素按一定规则进行分类,分散存储到多个redis实例中。
    对于一些榜单类的场景,用户一般只会访问前几百及后几百条数据,可以只缓存前几百条以及后几百条,即对用户经常访问的数据做缓存(正序倒序的前几页),而不是全部都做,对于获取中间的数据则可以直接从数据库获取
    一个集群存储了上亿的key
    如果key的个数过多会带来更多的内存空间占用,
    1、key本身的占用。
    2、集群模式中,服务端有时需要建立一些slot2key的映射关系,这其中的指针占用在key多的情况下也是浪费巨大空间。
    所以减少key的个数可以减少内存消耗,可以参考的方案是转Hash结构存储,即原先是直接使用Redis String 的结构存储,现在将多个key存储在一个Hash结构中

Redis的过期数据回收策略

Redis主要有2种过期数据回收策略:

惰性删除

惰性删除指的是当我们查询key的时候才对key进⾏检测,如果已经达到过期时间,则删除。显然,他有⼀个缺点就是如果这些过期的key没有被访问,那么他就⼀直⽆法被删除,⽽且⼀直占⽤内存。

定期删除

定期删除指的是Redis每隔⼀段时间对数据库做⼀次检查,删除⾥⾯的过期key。由于不可能对所有key去做轮询来删除,所以Redis会每次随机取⼀些key去做检查和删除。

Redis常见性能问题和解决方案

  1. Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。
  2. 如果数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
  3. 为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
  4. 尽量避免在压力较大的主库上增加从库。
  5. Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
  6. 为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。

有用请点赞,养成良好习惯!

疑问交流鼓励请留言!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐