一、猴子拿苹果问题

逛脉脉时,看到一网友遇到的面试题:有9个苹果,2只猴子。一个猴子每次拿2个苹果,一个猴子每次拿3个苹果。如果剩余的苹果数量不够猴子拿的数量,则停止拿苹果。请用多线程的方式模拟上面的描述。

看到问题的第一眼,觉得很有趣,脑海中第一个想到的就是通过信号量来实现,因为信号量是最适合做线程和进程状态同步了,而问题的考点就是线程同步。

恰好这两天刚好有在看信号量,所以很容易就想到通过信号量来实现这个问题。

其实原题说的是通过java多线程实现,但是我不会java,只能用c来写了。

为什么说考查的问题点是线程同步?因为题目要求是通过多线程来实现,如果不对线程状态制约,9个苹果,很容易在一瞬间就被一个猴子拿完,或许第二个线程还没开始苹果就已经拿完了。这明显不是面试官想看到的。所以不难想到考点就是如何协调两个猴子(线程)有序的拿桃子,有序的意思就是:你拿一次,我拿一次,要有次序的进行,直到把桃子拿完。如何控制线程之间你执行一次,我执行一次,肯定就要利用同步了。

二、匿名信号

匿名信号比较适用于线程间同步,因为它没有名字,所以多个进程间无法通过一个载体来共享它。只有在线程中,通过变量或者参数传递的方式来共享,所以适用于线程。

有名信号的用法参考:进程间通信之信号量

匿名信号的原理也和有名信号一样,维持一个计数器,然后通过等待计数器的状态来继续往下执行。相关的函数:

#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 发送信号量
int sem_post(sem_t *sem);
// 等待信号量
int sem_wait(sem_t *sem);
// 关闭信号量
int sem_close(sem_t *sem);

四个函数中,下面三个和有名信号量中的一样,唯一有区别的是第一个初始化信号量函数。匿名信号量无需传入信号量的名字,直接传入信号量对象就可以初始化了。后面的两个参数也都是一样,一个代表共享属性,一个代表默认值。

三、实现猴子拿苹果问题

两个猴子,肯定是需要两个线程,线程中辨别是哪个猴子肯定要传入猴子的参数,因此先实现一个猴子信息的对象,传入线程参数:

// 每个猴子的信息
struct monkey_info {
    int id; // 猴子的ID
    int cnt; // 每次拿苹果的数量
    sem_t *my; // 自己的信号量
    sem_t *other; // 另外一个猴子的信号量
};

为什么需要两个信号量呢,逻辑是:我去拿猴子,拿完通知你拿,你去拿猴子,我等在这里,等你拿完通知我拿。一个是等待自己的信号量,一个是通知另外猴子的信号量,所以需要两个。

线程函数实现:

// 苹果总量
static int s_apple_cnt = 9;

void *get_apple(void *ptr) {
    struct monkey_info *monkey = ptr;
    if (monkey == NULL)
        return NULL;

    while (1) {
        // 等待信号量
        sem_wait(monkey->my);
        if (s_apple_cnt < monkey->cnt) { // 当前的苹果数量不足以给到当前猴子
            printf("monkey %d: apple has't enough! total: %d, need: %d\n", monkey->id, s_apple_cnt, monkey->cnt);
            // 退出前,通知另外猴子继续拿
            sem_post(monkey->other);
            break;
        } else {
            // 拿苹果,减库存
            s_apple_cnt -= monkey->cnt;
            printf("monkey %d: get %d apple! remaining apples: %d\n", monkey->id, monkey->cnt, s_apple_cnt);
        }
        // 通知另外猴子
        sem_post(monkey->other);
    }
    return NULL;
}

主函数实现:

int main() {
    sem_t sem1, sem2;
    pthread_t tid1, tid2;
    // 初始化两个猴子的状态
    struct monkey_info monkey1 = {1, 2, &sem1, &sem2};
    struct monkey_info monkey2 = {2, 3, &sem2, &sem1};

    // 初始化信号
    sem_init(&sem1, 0, 1);
    sem_init(&sem2, 0, 0);

    // 创建线程
    pthread_create(&tid1, NULL, get_apple, (void *)&monkey1);
    pthread_create(&tid2, NULL, get_apple, (void *)&monkey2);

    // 等待线程退出
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    // 退出信号量
    sem_close(&sem1);
    sem_close(&sem2);

    return 0;
}

执行结果:

过程描述:

  1. 第一个猴子拿2个苹果,剩余7个苹果。
  2. 第二个猴子拿3个苹果,剩余4个苹果。
  3. 第一个猴子拿2个苹果,剩余2个苹果。
  4. 第二个猴子想要拿3个苹果,但是苹果不够了,退出。
  5. 第一个猴子拿两个苹果,苹果被拿完。
  6. 第一个猴子想要拿2个苹果,但是苹果不够了,退出。

共享内存是所有IPC通信中效率最高的,它通过把文件映射到用户进程空间,然后直接通过地址访问来实现多进程通信。相对于其他IPC通信方式而言,少去了把数据从用户空间复制到内核空间,再从内核空间复制到用户空间的过程,因此效率相当高。

用图形来表示就是:

操作共享内存的函数:

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t len);

第一个是映射共享内存的函数,参数说明:

  1. addr: 需要映射的共享内存起始地址,一般来说传入NULL。
  2. len: 映射出来的共享内存块大小。
  3. prot: 内存权限,长用的权限是PROT_READ | PROT_WRITE,表示可读可写。
  4. flags: 共享内存区域的属性,可以设置为MAP_SHARED或者MAP_PRIVATE,如果是前者,当前进程对共享内存区域的修改对其他进程可见,否则就不可见。
  5. fd: 共享内存文件的文件描述符。
  6. offset: 文件的偏移,从文件的offset处开始共享内存。

使用示例

以下代码展示了一个共享内存的操作示例,通过共享内存在父子进程间共享一个int类型的变量,然后父进程修改值,子进程读:

代码中忽略掉了一些不相关的错误处理。
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <semaphore.h>

int main() {
    int fd, n = 12345;
    int *ptr;
    sem_t *sem;
    pid_t pid;

    // 创建信号
    sem = sem_open("my_sem", O_RDWR | O_CREAT, 0755, 0);
    sem_unlink("my_sem");

    // 往文件写一个数据
    fd = open("mmapfile", O_RDWR | O_CREAT, 0755);
    write(fd, (void *)&n, sizeof(int));

    // 创建共享内存
    ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    pid = fork();
    if (pid == 0) {
        // 子进程,等待父进程修改完共享内存后打印出来
        sem_wait(sem);
        printf("child:\t%d\n", *ptr);
    } else if (pid > 0) {
        // 父进程,把共享内存加一
        printf("parent:\t%d\n", *ptr);
        (*ptr)++;
        sem_post(sem);
    } else {
        perror("fork error");
    }
    
    // 关闭信号量
    sem_close(sem);
    // 取消映射共享内存
    munmap(ptr, sizeof(int));
    // 删掉共享内存文件
    unlink("mmapfile");

    return 0;
}

代码中信号量的作用是确保子进程在父进程执行完成后才执行,父进程先把共享内存的数据加1,然后子进程读取共享内存的数据应该要变成12346。

执行结果符合预期:

一、信号量

信号量有两种,一种的有名信号,一种是无名信号。有名信号一般用于进程间同步,无名信号一般用于于线程间同步。创建或打开一个信号的函数:

#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ...);

name参数致命信号量的名字,由于信号量内部保存在系统内核中,多个进程间可以直接通过指定信号量的名字来相互通信。oflag表示信号的属性:可读、可写、可执行或者其他,如果指定了O_CREAT属性,则会创建信号量,否则表示打开已有信号量。后面的可选参数有两个:

  1. mode: 信号量的权限,和文件属性一样,0755或者其他。
  2. value: 默认值,信号量内部维持了一个计数,value就是设置计数的默认值。

操作信号量的方法:

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);

wait表示等待一个信号量,当信号量中的计数不为0时,会把计数减1,然后继续执行后面的逻辑,如果计数为0,wait操作就会阻塞,直到计数不为0。trywait是wait的非阻塞版本,它会尝试wait,如果计数为0也直接返回。post操作会把计数加1,它和wait相对。只有通过它把计数加1后,阻塞的信号量才最继续往下执行。

关闭信号量:

int sem_close(sem_t *sem);
int sem_unlink(const char *name);

close用于关闭信号量,这个操作会导致信号量的引用计数减1。当引用计数到达0后,执行过unlink的信号量会自动删除。

信号量的主要应用场景是同步,适合同步多个进程的状态同事到达某一个点。

二、示例代码

示例代码中创建了两个进程,每个进程都对一个变量执行三次自加操作。信号量的作用就是保证两个进程对变量自加操作的进度相同(意思是你自加了1次,我也自加1次,不存在你自加2次我才自加1次的情况)。

#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>

#define SEMAPHORE_CHILD_NAME "sem_child"
#define SEMAPHORE_PARENT_NAME "sem_parent"

int main() {
    int i, count;
    sem_t *sem_child, *sem_parent;
    pid_t pid;

    // 创建两个信号量,用于父子进程间相互制约
    sem_child = sem_open(SEMAPHORE_CHILD_NAME, O_RDWR | O_CREAT, 0755, 1);
    sem_parent = sem_open(SEMAPHORE_PARENT_NAME, O_RDWR | O_CREAT, 0755, 1);
    if (sem_child == NULL || sem_parent == NULL) {
        perror("sem_open error");
        return -1;
    }
    // 信号量使用完成后删除
    sem_unlink(SEMAPHORE_CHILD_NAME);
    sem_unlink(SEMAPHORE_PARENT_NAME);

    pid = fork();

    count = 0;
    if (pid == 0) {
        for (i = 0; i < 3; i++) {
            // 等待父进程信号量
            sem_wait(sem_parent);
            count++;
            // 设置子进程信号量
            sem_post(sem_child);
            printf("pid[%u]: count = %d\n", getpid(), count);
        }
    } else if (pid > 0) {
        for (i = 0; i < 3; i++) {
            // 等待子进程信号量
            sem_wait(sem_child);
            count++;
            // 发送父进程信号量
            sem_post(sem_parent);
            printf("pid[%u]: count = %d\n", getpid(), count);
        }
    } else {
        perror("fork error");
    }

    // 关闭信号量
    sem_close(sem_parent);
    sem_close(sem_child);

    return 0;
}

执行结果:

可以看到,两个进程间的字节都是交替进行的。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/word-frequency

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

写一个 bash 脚本以统计一个文本文件 words.txt 中每个单词出现的频率。

为了简单起见,你可以假设:

  • words.txt只包括小写字母和 ' ' 。
  • 每个单词只由小写字母组成。
  • 单词间由一个或多个空格字符分隔。

示例:

假设 words.txt 内容如下:

the day is sunny the the
the sunny is is

你的脚本应当输出(以词频降序排列):

the 4
is 3
sunny 2
day 1

说明:

  • 不要担心词频相同的单词的排序问题,每个单词出现的频率都是唯一的。
  • 你可以使用一行 Unix pipes 实现吗?

二、题解

2.1 使用awk

通过NF变量遍历所有字段,存到一个哈希表(数组)中,然后打印出所有的key-value组合,最后通过sort排序。

awk '{for (i = 1; i <= NF; i++) {m[$i]++;}} END {for (i in m) {print i, m[i]}}' words.txt | sort -nr -k 2

2.2 使用xargs

通过xargs的-n参数打印出所有的字段,然后使用uniqsort对字段排序:

cat file.txt | xargs -n 1 | sort | uniq -c | sort -nr -k 2 | awk '{print $2" "$1}'
uniq的-c参数是统计词频

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/tenth-line

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个文本文件 file.txt,请只打印这个文件中的第十行。

示例:

假设 file.txt 有如下内容:

Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10

你的脚本应当显示第十行:

Line 10

说明:

  1. 如果文件少于十行,你应当输出什么?
  2. 至少有三种不同的解法,请尝试尽可能多的方法来解题。

二、题解

2.1 tail+head

返回文本中的第十行,最简单的方式可以通过tail + head配合输出,通过head取到前面10行,然后输出前面10行的最后一行。很容易能想到的办法:

head -10 file.txt | tail -1

但这么做就错了,题目中已经提到如果文件少于10行,你应当输出什么?。一旦文件少于10行,上面的命令会输出文件的最后一行,不满足题意。正确的命令:

tail -n +10 file.txt | head -1

-n +10表示从第10行开始到结束,意思是先从第10行开始取到结束的所有数据然后取第一行。

2.2 sed

sed的p模式可以直接打印指定行:

sed -n '10p' file.txt

2.3 awk

awk内置变量NR表示行数

awk 'NR==10' file.txt

一、题目描述

Description

Write a program to convert numbers in one base to numbers in a second base. There are 62 different digits: { 0-9,A-Z,a-z }
HINT: If you make a sequence of base conversions using the output of one conversion as the input to the next, when you get back to the original base, you should get the original number.

Input

The first line of input contains a single positive integer. This is the number of lines that follow. Each of the following lines will have a (decimal) input base followed by a (decimal) output base followed by a number expressed in the input base. Both the input base and the output base will be in the range from 2 to 62. That is (in decimal) A = 10, B = 11, ..., Z = 35, a = 36, b = 37, ..., z = 61 (0-9 have their usual meanings).

Output

The output of the program should consist of three lines of output for each base conversion performed. The first line should be the input base in decimal followed by a space then the input number (as given expressed in the input base). The second output line should be the output base followed by a space then the input number (as expressed in the output base). The third output line is blank.

Sample Input

8
62 2 abcdefghiz
10 16 1234567890123456789012345678901234567890
16 35 3A0C92075C0DBF3B8ACBC5F96CE3F0AD2
35 23 333YMHOUE8JPLT7OX6K9FYCQ8A
23 49 946B9AA02MI37E3D3MMJ4G7BL2F05
49 61 1VbDkSIMJL3JjRgAdlUfcaWj
61 5 dl9MDSWqwHjDnToKcsWE1S
5 10 42104444441001414401221302402201233340311104212022133030

Sample Output

62 abcdefghiz
2 11011100000100010111110010010110011111001001100011010010001

10 1234567890123456789012345678901234567890
16 3A0C92075C0DBF3B8ACBC5F96CE3F0AD2

16 3A0C92075C0DBF3B8ACBC5F96CE3F0AD2
35 333YMHOUE8JPLT7OX6K9FYCQ8A

35 333YMHOUE8JPLT7OX6K9FYCQ8A
23 946B9AA02MI37E3D3MMJ4G7BL2F05

23 946B9AA02MI37E3D3MMJ4G7BL2F05
49 1VbDkSIMJL3JjRgAdlUfcaWj

49 1VbDkSIMJL3JjRgAdlUfcaWj
61 dl9MDSWqwHjDnToKcsWE1S

61 dl9MDSWqwHjDnToKcsWE1S
5 42104444441001414401221302402201233340311104212022133030

5 42104444441001414401221302402201233340311104212022133030
10 1234567890123456789012345678901234567890

Source

Greater New York 2002

二、题目解析

题目的意思是实现62进制内任意进制数据的转换,其中A-Z表示10-35,a-z表示36-62。

这道题目一共考察了两个问题点,第一个是进制转换,第二个是大数据处理。更倾向于考察第二个问题点。

看了很多题解,百度上的题解基本都是copy的,一模一样的代码套进去,都没有说明白为什么要这样,没有任何价值。最后是google到的一篇题解才真正明白,其实解题的思路很简单,就是实现进制转换的短除法,然后处理除法过程中的大数据运算。

2.1 进制转换

解题前首先要搞清楚进制之间的转换逻辑,A进制转B进制如何转?

一般的思路是先从当前进制转到10进制,再从10进制转成目标进制。

从其他进制转换成十进制的办法是按权相加:

从十进制转成其他进制的办法是短除法:

2.2 大数计算

除了进制转换以外,最重要的功能就是除法的实现了,如何通过除法来获取余和商。

因为题目给的数据范围过大,如果用整形来保存数据,肯定是不够的,即使是long long或者更大的类型,都不足以容纳下题目所要求的数据范围。因此,数据只能用字符数组来保存,每个数组元素表示1位数字。

那么这道题目就转变成了通过字符串数组来求商和余数的问题了,只要循环求出所有的商和余数,问题就解决了。

三、代码

代码一共分为几个步骤:

  1. 将字符数组中的每个元素都转成整形,方便运算。
  2. 对每个元素求商和余数,模拟短除法,记录余数。
  3. 反转所有的余数,转成字符形式,输出。

第一部分,62进制字符和数字之间的相互转换:

// char转整形
static int char2int(char ch) {
    if (ch >= '0' && ch <= '9')
        return ch - '0';
    else if (ch >= 'a' && ch <= 'z')
        return ch - 'a' + 36;
    else
        return ch - 'A' + 10;
}

// 整形转char
static char int2char(int n) {
    if (n >= 0 && n <= 9)
        return '0' + (char) n;
    else if (n >= 10 && n <= 35)
        return 'A' + n - 10;
    else
        return 'a' + n - 36;
}

第二部分,任意进制转换函数:

char *base_conversion(int from, int to, const char *src, char *ans) {
    int len, i, j, idx;
    unsigned int tmp[MAX_SIZE], ans_rev[MAX_SIZE];
    len = strlen(src);

    // 将字符串转成整形
    for (i = 0; i < len; i++) {
        tmp[i] = (char)char2int(src[i]);
    }
    tmp[len] = '\0';

    idx = 0;
    for (i = 0; i < len;) {
        // 短除法,辗转相除,从高位往低位除
        // 假设当前字符串是123,下面就是让123除2得到商61和余1的过程
        // 下一次再走进这里就是通过63取得商31和余1的过程
        for (j = i; j < len - 1; j++) {
            tmp[j + 1] += tmp[j] % to * from;
            tmp[j] = tmp[j] / to;
        }
        
        // 最后一位取余数,ans_rev是逆序排放的余数
        ans_rev[idx++] = tmp[len - 1] % to;
        tmp[len - 1] /= to;
        
        // tmp[i]现在是商,忽略掉头部的0
        while (i < len && !tmp[i])
            i++;
    }

    // 反转所有的余并转成字符形式
    ans[idx] = '\0';
    for (j = 0; j < idx; j++) {
        ans[j] = int2char(ans_rev[idx - j - 1]);
    }
    return ans;
}

第三部分,main函数:

int main() {
    char input[MAX_SIZE], output[MAX_SIZE];
    int n, i = 0, from, to;
    scanf("%d", &n);
    while (++i < n) {
        scanf("%d %d %s", &from, &to, input);
        base_conversion(from, to, input, output);
        printf("%d %s\n%d %s\n\n",from, input, to, output);
    }
    return 0;
}
要注意的一个问题是输出格式是三行,每行输出后面都要有一个空行。

单元测试案例

// 测试char和int互转
TEST(base_conversion, int2char_and_char2int) {
    char map_arr[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    int i;
    for (i = 0; i < 62; i++) {
        EXPECT_EQ(i, char2int(map_arr[i]));
    }
    for (i = 0; i < 62; i++) {
        EXPECT_EQ(map_arr[i], int2char(i));
    }
}

// 测试进制转换,数据来源样例输入
TEST(base_conversion, conversion) {
    char input[MAX_SIZE] = { 0 }, output[MAX_SIZE] = { 0 };
    strcpy(input, "103");
    EXPECT_STREQ(base_conversion(10, 2, input, output), "1100111");
    EXPECT_STREQ(base_conversion(62, 2, "abcdefghiz", output), "11011100000100010111110010010110011111001001100011010010001");
    EXPECT_STREQ(base_conversion(10, 16, "1234567890123456789012345678901234567890", output), "3A0C92075C0DBF3B8ACBC5F96CE3F0AD2");
    EXPECT_STREQ(base_conversion(16, 35, "3A0C92075C0DBF3B8ACBC5F96CE3F0AD2", output), "333YMHOUE8JPLT7OX6K9FYCQ8A");
    EXPECT_STREQ(base_conversion(35, 23, "333YMHOUE8JPLT7OX6K9FYCQ8A", output), "946B9AA02MI37E3D3MMJ4G7BL2F05");
    EXPECT_STREQ(base_conversion(23, 49, "946B9AA02MI37E3D3MMJ4G7BL2F05", output), "1VbDkSIMJL3JjRgAdlUfcaWj");
    EXPECT_STREQ(base_conversion(49, 61, "1VbDkSIMJL3JjRgAdlUfcaWj", output), "dl9MDSWqwHjDnToKcsWE1S");
}

EAGAINEWOULDBLOCK是linux环境下的两个错误码,在非阻塞IO中经常会碰到,对新手而言,如何处理这两个值非常头疼。如果处理不当,很容易导致程序异常。

EAGAIN的官方定义:

“Resource temporarily unavailable.” The call might work if you try again later. The macro EWOULDBLOCK is another name for EAGAIN; they are always the same in the GNU C Library.

翻译:资源短暂不可用,这个操作可能等下重试后可用。它的另一个名字叫做EWOULDAGAIN,这两个宏定义在GNU的c库中永远是同一个值。

EWOULDBLOCK的定义:

“Operation would block.” In the GNU C Library, this is another name for EAGAIN (above). The values are always the same, on every operating system.

翻译:操作将会被阻塞,在GNU C的库中,它的另外一个名字是EAGAIN,在任何操作系统中他们两个的值都是一样的。

这两个错误码在大多数系统下是都同一个东西,特别是在使用了GNU的libc库平台(目前广泛使用的centos和ubuntu都是)下一定是相同的。这个错误产生的情况:

  • 尝试在一个设置了非阻塞模式的对象上执行阻塞操作,重试这个操作可能会阻塞直到其他条件让它可读、可写或者其他操作。
  • 对某些操作来说,资源短暂不可用。例如fork函数可能返回这个错误(当没有足够的资源能够创建一个进程时),可以采取的操作是休息一段时间然后再继续操作。

那么应该如何处理这个错误?

最好的办法是重试,重试一定次数后还不成功就退出操作。

为什么不能无限重试呢?假设在通过socket发送一段数据,发送缓冲区如果一直不可写,就会出现无限循环的情况,进程卡死。

其他

在某些较老的unix系统上,这两个值的意义可能不一样。

可以参考:Which systems define EAGAIN and EWOULDBLOCK as different values?

参考

Difference between 'EAGAIN' or 'EWOULDBLOCK'

Error Codes

一、索引类型

索引根据底层实现可分为B-Tree索引和哈希索引,大部分时候我们使用的都是B-Tree索引,因为它良好的性能和特性更适合于构建高并发系统。

根据索引的存储方式来划分,索引可以分为聚簇索引非聚簇索引。聚簇索引的特点是叶子节点包含了完整的记录行,而非聚簇索引的叶子节点只有所以字段和主键ID。

根据聚簇索引和非聚簇索引还能继续下分还能分为普通索引、覆盖索引、唯一索引以及联合索引等。

- 阅读剩余部分 -

一、AOF持久化

1.1 实现机制

AOF(Append Only File)是redis持久化方式的一种,它通过把所有redis执行过的命令都写入到文件来维持持久化。一旦服务崩溃,则可以重放这些命令来恢复服务数据。

例如,在redis上执行下面2条语句:

127.0.0.1:6379> set data "helloworld"
OK
127.0.0.1:6379> sadd zdata hello world
(integer) 2

那么AOF文件中的内容就类似是:

set data "helloworld"
sadd zdata hello world

当然,文件中保存不是直接的命令语句。而是按照redis命令请求协议保存的,除了执行的命令以外还有一些其他的内容也会保存到文件。redis协议是redis执行命令专用的一种协议,当客户端向服务端发送请求也是使用的redis协议,AOF也使用redis协议的好处是可以直接使用redis协议解析库,不用再单独实现一套。

AOF数据并不是实时写入到文件中的,而是优先保存在缓冲区。redisServer对象中保存了一个aof_buf字段,它就是aof命令的保存缓冲区。当执行完命令后,会先把指令保存到这里,然后根据策略把缓冲区中的内容写入到磁盘。一个要注意的是,文件写入磁盘并不会立马刷新到文件系统,因为操作系统对系统IO有缓存,缓存到达一定条件后才会同步缓存到文件系统。

为了避免数据没有及时写入到文件系统(还在缓存中),AOF提供了三种策略来同步缓存:

策略描述
always每处理一次事件循环,就把aof文件写入到文件。这种办法最稳妥,数据丢失后的恢复效果最好。
Everysec每秒同步一次AOF文件缓存到文件系统。
no不主动同步缓存,由操作系统决定什么时候写入到文件系统。

1.2 AOF重写

当服务运行久了之后,AOF文件会变得很大,redis提供了AOF重写功能来优化AOF文件数据。

所做的优化就是整合指令,例如:

127.0.0.1:6379> zadd age 14 hello 15 world 16 nginx
(integer) 3
127.0.0.1:6379> zrem age hello
(integer) 1

执行完这两个命令后,数据库保存了一个有序集合age,里面包含了两个元素world和nginx。其中hello在第一个命令中加进来了,但是第二个指令又删除了,所以实际上写入AOF文件并不需要上面两行,只需要一行就可以完成:

zdd age 15 world 16 nginx

AOF重写就是基于这种策略来重写的,但是有一个要注意的地方是:在执行AOF写入的时候,会导致redis阻塞。所以一般建议是使用后台重写来完成整个AOF文件的重写,后台重写会新建一个子进程,避免在父进程中操作阻塞正常业务。执行后台重写的指令是BGREWRITEAOF

二、RDB持久化

RDB持久化是redis的第二种持久化方式,它持久化的原理是把数据库中所有键值对的二进制数据直接写入到文件。RDB持久化的优先级一般低于AOF持久化,因为AOF持久化的实时性高于RDB,所以在系统启动的时候如果存在AOF持久化,redis会优先通过AOF持久化恢复数据。

执行持久化的操作指令是SAVEBGSAVE,一个是前台执行,一个是后台执行。和AOF持久化一样,前台执行会阻塞当前redis进程,所有客户断请求都会被阻塞,因此一般建议是用BGSAVE,它也会新创建一个子进程来做持久化操作。RDB备份是全量备份。

可以在redis.conf中配置RDB相关的操作,主要有以下几个配置:

save <seconds> <changes>
dbfilename dump.rdb
dir ./

第一个配置是执行BGSAVE的策略,当在secnods秒之内执行了changes次操作时,会自动把改动写入到RDB文件。可以同时有多个save配置段存在,例如官方默认的配置中就有三个save配置:

save 900 1
save 300 10
save 60 10000

这三个配置,只要在任意时间段内满足了任意一条就会执行(三者互不影响,互相独立,各自备份各自的):

  1. 900秒内执行了一次更新,BGSAVE就会执行。
  2. 300秒内执行了10次更新,BGSAVE就会执行。
  3. 60秒呢执行了10000次更新,BGSAVE就会执行。

dbfilenamedir指定了备份文件名和路径,执行后就会把RDB文件写入到这个文件中。例如,设置保存的路径为/appdata/redis/dump.rdb,执行save命令后,就会生成这个文件:

RDB持久化文件可以使用官方的redis-check-rdb程序(低版本叫redis-check-dump)来检测相关数据:

当然,这个文件只提供了基本检测功能(即验证rdb文件是否合法),并不包含导出所有数据的功能。

如果需要处理实际的数据可以通过其他工具来实现,google一下redis rdb parser就能看到了。

github上也有很多,例如https://github.com/sripathikrishnan/redis-rdb-tools

三、AOF和RDB对比

RDB持久化AOF持久化
全量备份,一次保存整个数据库增量备份,只保存新修改的数据
保存的间隔较长保存的间隔默认一秒
更适合数据备份,默认开启更适合用来保存数据,和一般SQL持久化方式一样,默认关闭
save会阻塞,但bgsave或者自动不会阻塞无论是平时还是AOF重写,都不会阻塞
轻重 : 重轻重: 轻
启动优先级 : 低启动优先级 : 高
体积 : 小体积 : 大
恢复速度 : 快恢复速度 : 慢
数据安全性 : 丢数据数据安全性 : 根据策略决定
表格从Redis持久化AOF和RDB对比整理得来。

当使用的内存到达上限后,redis提供了6种策略来淘汰键值:

策略描述
volatile-lru在所有设置了过期时间的键值中根据LRU算法淘汰最近最少使用的
allkeys-lru对数据库中所有元素根据LRU算法淘汰最近最少使用的
volatile-random从设置了过期时间的元素中随机淘汰
allkeys->random数据库所有元素中随机淘汰
volatile-ttl从设置了过期时间的键值中淘汰快要超时的
noeviction不淘汰任何已有键值,直接给写操作返回错误
LRU是最近最少使用的,直译出来就是最久没有使用的。

redis默认的淘汰策略是volatile-lru,修改淘汰策略可以通过修改redis.conf文件中的maxmemory-policy字段,配置中关于各种淘汰策略也有详细的解释。使用grep volatile-lru redis.conf -A 6 -n可以过滤出这部分配置 :