Redis

database

# 一、Redis基础

Redis由C语言开发,基于内存亦可持久化的高性能key-value数据库。单机QPS可抗十万并发。

# Redis数据结构

  • String

  • Hash

  • List

  • Set

  • Zset

# Redis持久化

RDB Redis DataBase

AOF Append Only File

Redis作为一个具备有持久化功能的缓存数据库,所以其一定会有一个用于数据存储的目录。那么一般在Redis处理的时候会有三类文件需要做保存:

  1. Redis运行时的pid:run
  2. Redis相关处理日志:logs
  3. Redis的数据文件:dbcache

所以建立一个目录用于存放这些数据。

mkdir -p /usr/data/redis/{run,logs,dbcache}
1

# RDB

RDB Redis Database

RDB指在指定的时间间隔内,将全量缓存数据生成一个快照来备份,写入二进制文件中(dump.rdb)。Redis默认启用RDB快照持久化。

1、在redis.conf中配置:

save <间隔时间(秒)> <写入此时>
1

例如:

save 900 1
save 300 10
save 60 10000
1
2
3

在900秒内,数据集至少有1个改动,则自动快照保存。

缺点:无法实时持久化。

2、还可以手动执行命令进行持久化。

  • save
  • bgsave

每次命令执行,都将当前快照到一个新的rdb文件,并覆盖原有rdb快照文件。

bgsave

background save

bgsave,写时复制机制(COW),异步。Redis借助操作系统的写时复制技术(COW),在生成快照的同时,还可以正常处理写命令。Redis默认就使用bgsave方式进行RDB持久化。

bgsave子县程是由主线程

# AOF

AOP Append Only File

将Redis每一个写命令都通过write追加至文件中,通俗理解为日志记录。

优点:支持秒级持久化,兼容性好,数据不易丢失;

缺点:文件大,恢复速度慢;

# Redis事务

Redis支持事务机制。其本质是一组命令的集合,有3个阶段:开始事务、命令入队、执行事务。

# Redis事务命令

操作 命令
打开事务 multi
取消事务 discard
提交事务 exec
取消watch对所有key的监控 unwatch

# Redis事务与MySQL事务的区别

1、Redis事务不保证原子性;

2、没有隔离级别的概念;

3、Redis事务中,若在事务队列存在命令性错误,则都不执行;若存在语法性错误,则错误命令抛出异常,而正常命令正常执行。

# watch指令

watch指令类似于乐观锁,使用watch指令检测key后,如果此时key的值被其他客户端更改,当提交事务是,会提示返回(nil),表示事务执行失败。

# Redis管道pipeline

客户端可以一次性发送多个命令给服务器(打包发送)。

特点

  • 减少网络开销;
  • 不具备原子性。若执行失败,得到响应后继续执行下一条命令。

使用

Pipeline pl = jedis.pipelined();
1

# Redis lua脚本

Redis2.6推出脚本功能,允许使用Lua脚本传到Redis中执行。通过内置的Lua解释器,使用EVAL命令对Lua脚本求值。

Lua脚本可以一次性发送多条命令给服务器。

Redis官方推荐使用Redis Lua代替Redis事务。

特点

  1. 减少网络开销;
  2. 原子性;
  3. 替代Reids事务,支持错误回滚。

注意:

此操作需要防止代码中写入死循环或其他耗时操作等,不然Redis会阻塞,将不接受其他命令。

# Redis分布式锁

要满足Redis分布式锁,需满足互斥性、不能死锁、容错性。

# Redis连接池

# Redis删除机制

# Redis发布订阅机制

  • 发布消息:publish channel message
  • 订阅:subscribe channel

# 二、Redis集群

集群模式 Redis节点数(一般情况) 优点 缺点
Redis主从 3 数据备份 宕机无法恢复
Redis哨兵 3+3 自动故障转移 1、维护麻烦;2、配置文件会随之更改;3、无法分片;
Redis Cluster 6 1、分片;2、扩容机制;

# Redis主从

# Redis哨兵

# Redis Cluster

# 三、Redis高级

# Redis线程模型

  1. Redis客户端与Redis服务端建立Socket长连接,发送读写事件。

  2. IO多路复用器只进行监听请求不进行处理请求。其同时监听多个Socket的事件请求(连接accept、读read、写write、关闭close),有事件产生就将请求压入队列中(非阻塞)。

  3. 文件事件分配器根据事件的类型,来选择对应的事件处理器进行处理。

  4. 事件处理器有以下三种:

    • 命令请求处理器,处理写请求,写数据到Redis;

    • 命令回复处理器,处理读请求,读取数据给Redis客户端;

    • 连接应答处理器,处理连接请求,客户端与服务器建立socket通信连接;

# Redis缓存穿透

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。

比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

解决方案:

最常见的则是采用布隆过滤器(Bloom Filter)

过滤器:二进制数组,长度为N,初始化值都为0。

将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存穿透无法彻底解决,我们只能将其控制在可容忍的范围内。

二进制数组构建过程

1、加载符合条件的记录;

2、计算每条元素key的hash值;

3、计算hash值对应数组中的位置;

4、将该位置的值改为1;

查找元素是否存在过程

1、计算元素key的hash值;

2、计算hash值对应数组的位置;

3、找到该位置的值,0代表着不存在,1代表可能存在;

# Redis缓存击穿

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案:

  1. 使用互斥锁(mutex key);

# 互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

SETNX

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

若key存在返回0;不存在返回1。

# Redis缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

解决方案:

这里这边总结了几种解决方案:

  1. Redis 高可用集群

    缓存存储高可用比如Rcds集群,这样就能防止某台 Redis挂掉之后所有缓存丢失导致的雪崩问题。

  2. 合理管理缓存失效时间,防止集中失效

    缓存失效时间要设计好,不同的数据有不同的有效期尽量保证不要在同一时间失效,统一去规划有效期,让失效时间分布均匀即可。

  3. 对热门数据,采用定时更新,避免自动失效

    对于一些热门数据的持续读取,这种缓存数据也可以采取定时更新的方式来刷新缓存,避免自动失效。

  4. 限流降级

    服务限流和接口限流,如果服务和接口都有限流机制,就算缓存全部失效了,但是请求的总量是有限制的,可以在承受范围之内,这样短时间内系统响应慢点,但不至于挂掉,影响整个系统。

  5. 加锁

    从数据库获取缓存需要的数据时加锁控制本地锁或者分布式锁都可以。当所有请求都不能命中缓存,这就是我们之前讲的缓存穿透,这时候要去数据库中查询,如果同时并发的量大,也是会导致雪崩的发生,我们可以在对数据库查询的地方进行加锁控制,不要让所有请求都过去,这样可以保证存储服务不挂掉。

    加锁的本质还是控制并发量,不要让所有请求瞬时压到数据库上面去,加了锁就以为着性能要丢失一部分。

    1. 使用JVM提供的本地锁synchronized;

    2. 分布式锁,分布式锁常用有2种:基于Redis和 Zookeeper的实现。

    示例使用Redisson提供的基于Redis实现的分布式锁。

    RLock lock = redisson.getLock("anyLock");
    // 支持过期解锁功能,10秒钟后无需调用unlock方法自动解锁
    lock.lock(10, TimeUnit.SECONDS);
    
    1
    2
    3

# Redis数据库双写不一致

缓存中的数据与数据库中的数据不一致。

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:

  • 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
  • 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

解决方案:

  1. 采用延时双删策略
  2. 异步更新缓存(基于订阅binlog的同步机制);

# 1、延时双删策略

第一种方案:采用延时双删策略;在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。伪代码如下:

public void write(String key,Object data){
 redis.delKey(key);
 db.updateData(data);
 Thread.sleep(500);
 redis.delKey(key);
 }
1
2
3
4
5
6

具体的步骤

1)先删除缓存

2)再写数据库

3)休眠500毫秒

4)再次删除缓存

那么,这个500毫秒怎么确定的,具体该休眠多久呢?

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

当然这种策略还要考虑redis和数据库主从同步的耗时。最后写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。

设置缓存过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

该方案的弊端

结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

# 2、异步更新缓存

第二种方案:异步更新缓存(基于订阅binlog的同步机制)

技术整体思路

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

1)读Redis:热数据基本都在Redis

2)写MySQL:增删改都是操作MySQL

3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

Redis更新

1)数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis);

  • 一个是增量(实时更新)。这里说的是增量,指的是mysql的update、insert、delate变更数据。

2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

# Redis热点数据

# 四、Redis安装

# Windows安装

# Docker安装

# 单机版standalone

docker-compose.yml

mkdir -p /usr/local/docker/redis-standalone
cd /usr/local/docker/redis-standalone
vi docker-compose.yml
1
2
3

docker-compose.yml文件内容:

version: '3.3'
services:
  redis-standalone:
    image: redis
    container_name: redis-standalone
    restart: always
    ports:
      - 6379:6379
    volumes:
      - ./redis.conf:/etc/redis/redis.conf:rw
      - ./data/:/data:rw
    command:
      redis-server /etc/redis/redis.conf --appendonly yes
1
2
3
4
5
6
7
8
9
10
11
12
13

redis.conf

requirepass wszgr
appendonly yes
1
2

运行容器

docker-compose up -d
docker-compose logs -f
docker ps
1
2
3

# 集群版cluster

# 五、Redis客户端

  • 官方提供命令行客户端:redis-cli Redis
  • Java客户端:Jedis、Lettuce、Redisson

# 六、Redis使用

# 注解使用

注解 用途
@Cacheable 查询
@CacheEvict 删除
@CachePut 修改