欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

深入理解Redis

程序员文章站 2022-03-05 10:12:41
...

Redis 单线程如何处理高并发

阻塞 IO 与非阻塞 IO

在阻塞模式下,如果你从数据流中读取不到指定大小的数据量,IO 就会阻塞。比如已知会有 10 个字节发送过来,但是我目前只收到 4 个,还剩六个,此时就会发生阻塞。如果是非阻塞模式,虽然此时只收到 4 个字节,但是读到 4 个字节就会立即返回,不会傻傻等着,等另外 6 个字节来的时候,再去继续读取。

所以阻塞IO性能低于非阻塞IO

如果有一个 Web 服务器,使用阻塞 IO 来处理请求,那么每一个请求都需要开启一个新的线程;但是如
果使用了非阻塞 IO,基本上一个小小线程池就够用了,因为不会发生阻塞,每一个线程都能够高效利
用。

Redis的线程模型

首先一点,redis是单线程,单线程如何解决高并发问题?

实际上,能够处理高并发的单线程不仅仅是Redis,除了Redis之外,还有NodeJS,Nginx等等也是单线程的。

Redis虽然是单线程但是运行很快,主要有如下几个方面原因:

  1. Redis的所有数据结构是基于内存的,所有的计算也是基于内存级别的计算,所以快
  2. Redis是单线程,所以有一些时间复杂度高的指令,可能会导致Redis卡顿,例如keys。
  3. Redis在处理并发的客户端连接时,使用了非阻塞IO

在使用非阻塞IO时,有一个问题,那就是线程如何知道剩下的数据来了?

这里就涉及到一个新的概念叫做多路复用,本质上就是一个事件轮询API

  1. Redis会给每一个客户端指令通过队列来排队进行顺序处理
  2. Redis做出响应时,也有一个响应的队列

Redis的通讯协议

Redis 通信使用了文本协议,文本协议比较费流量,但是 Redis 作者认为数据库的瓶颈不在于网络流量,而在于内部逻辑,所以采用了这样一个费流量的文本协议。
这个文本协议叫做 Redis Serialization Protocol,简称 RESP。
Redis 协议将传输的数据结构分为 5 种最小单元,单元结束时,加上回车换行符 \r\n。

  1. 单行字符串以+开始,例如:+javaboy\r\n
  2. 多行字符串以$开始,后面加上字符串长度,例如:$11\r\njavaboy.org\r\n
  3. 整数以:开始,例如::1024\r\n
  4. 错误消息以-开始
  5. 数组以*开始,后面加上数组长度

实战

为了方便客户端连接Redis,我们关闭Redis保护模式(在redis.conf文件中)

protected no

同时关闭密码:

# requirepass xxxx

配置完成后,重启Redis

接下来,我们通过 Socket+RESP 来定义两个最最常见的命令 set 和 get。

Redis持久化

两种持久化方案:

  1. 快照rdb
  2. AOF日志

快照RDB

原理

Redis使用操作系统的多线程机制来实现快照持久化:Redis在持久化时,会调用glibc函数fork一个子进程,然后将快照持久化操作完全交给子进程去处理,而父进程则继续处理客户端的请求,在这个过程中,子进程能够看到内存中的数据在子进程的产生的一瞬间就固定下来了,再也不会改变,也就是为什么Redis持久化叫做快照

具体配置

在 Redis 中,默认情况下,快照持久化的方式就是开启的。
默认情况下会产生一个 dump.rdb 文件,这个文件就是备份下来的文件。当 Redis 启动时,会自动的去
加载这个 rdb 文件,从该文件中恢复数据。
具体的配置,在 redis.conf 中:

# 表示快照的频率,第一个表示 900 秒内如果有一个键被修改,则进行快照
save 900 1
save 300 10
save 60 10000
# 快照执行出错后,是否继续处理客户端的写命令
stop-writes-on-bgsave-error yes
# 是否对快照文件进行压缩
rdbcompression yes
# 表示生成的快照文件名
dbfilename dump.rdb
# 表示生成的快照文件位置
dir ./

备份流程

  1. 在 Redis 运行过程中,我们可以向 Redis 发送一条 save 命令来创建一个快照。但是需要注意,
    save 是一个阻塞命令,Redis 在收到 save 命令开始处理备份操作之后,在处理完成之前,将不再
    处理其他的请求。其他命令会被挂起,所以 save 使用的并不多。
  2. 我们一般可以使用 bgsave,bgsave 会 fork 一个子进程去处理备份的事情,不影响父进程处理客
    户端请求。
  3. 我们定义的备份规则,如果有规则满足,也会自动触发 bgsave。
  4. 另外,当我们执行 shutdown 命令时,也会触发 save 命令,备份工作完成后,Redis 才会关闭。
  5. 用 Redis 搭建主从复制时,在 从机连上主机之后,会自动发送一条 sync 同步命令,主机收到命令
    之后,首先执行 bgsave 对数据进行快照,然后才会给从机发送快照数据进行同步。

AOF

与快照持久化不同,AOF持久化是将被执行的命令追加到aof文件的末尾,在恢复时,只需要把记录下来的命令从头到尾执行一遍即可。

默认情况下,AOF 是没有开启的。我们需要手动开启:

# 开启 aof 配置
appendonly yes
# AOF 文件名
appendfilename "appendonly.aof"
# 备份的时机,下面的配置表示每秒钟备份一次
appendfsync everysec
# 表示 aof 文件在压缩时,是否还继续进行同步操作
no-appendfsync-on-rewrite no
# 表示当目前 aof 文件大小超过上一次重写时的 aof 文件大小的百分之多少的时候,再次进行重写
auto-aof-rewrite-percentage 100
# 如果之前没有重写过,则以启动时的 aof 大小为依据,同时要求 aof 文件至少要大于 64M
auto-aof-rewrite-min-size 64mb

同时为了避免快照备份的影响,记得将快照备份关闭:

save ""
#save 900 1
#save 300 10
#save 60 10000

Redis事务

正常来说,一个可以商用的数据库往往都有比较完善的事务支持,Redis 当然也不例外。相对于 关系型
数据库中的事务模型,Redis 中的事务要简单很多。因为简单,所以 Redis 中的事务模型不太严格,所
以我们不能像使用关系型数据库中的事务那样来使用 Redis。
在关系型数据库中,和事务相关的三个指令分别是:

  • begin
  • commit
  • rollback

在 Redis 中,当然也有对应的指令:

  • multi
  • exec
  • discard

原子性

注意,Redis 中的事务并不能算作原子性。它仅仅具备隔离性,也就是说当前的事务可以不被其他事务打断。
由于每一次事务操作涉及到的指令还是比较多的,为了提高执行效率,我们在使用客户端的时候,可以通过 pipeline 来优化指令的执行。
Redis 中还有一个 watch 指令,watch 可以用来监控一个 key,通过这种监控,我们可以确保在 exec之前,watch 的键的没有被修改过。

java代码实现

public class TransactionTest {
    public static void main(String[] args) {
            new Redis().execute(jedis -> {
            new TransactionTest().saveMoney(jedis, "javaboy", 1000);
        });
    }
    public Integer saveMoney(Jedis jedis, String userId, Integer money) {
        while (true) {
            jedis.watch(userId);
            int v = Integer.parseInt(jedis.get(userId)) + money;
            Transaction tx = jedis.multi();
            tx.set(userId, String.valueOf(v));
            List<Object> exec = tx.exec();
            if (exec != null) {
            	break;
            }
        }
        return Integer.parseInt(jedis.get(userId));
    }
}
相关标签: redis