分类 数据结构和算法 下的文章

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/permutations

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

一、题目描述

给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:

  • 输入: [1,2,3]
  • 输出:

    [
      [1,2,3],
      [1,3,2],
      [2,1,3],
      [2,3,1],
      [3,1,2],
      [3,2,1]
    ]

二、题解

求全排列,简单的深搜题目,解法参考:深度优先搜索

三、代码

class Solution {
private:
    set<int> m_set;
    vector<int> m_vec;
    vector<vector<int>> m_res;

    void dfs(vector<int> &nums, int l, int r) {
        int i;
        if (l == r) {
            m_res.push_back(m_vec);
            return;
        }

        for (i = 0; i < nums.size(); i++) {
            set<int>::iterator it;
            if (m_set.find(nums[i]) != m_set.end())
                continue;

            // 没有找到
            m_vec.push_back(nums[i]);
            m_set.insert(nums[i]);

            // 深搜
            dfs(nums, l + 1, r);

            // 删除当前值,遍历下一个节点
            m_vec.pop_back();
            it = m_set.find(nums[i]);
            if (it == m_set.end())
                throw logic_error("cannot find key");
            m_set.erase(it);
        }
    }

public:
    vector<vector<int>> permute(vector<int> &nums) {
        dfs(nums, 0, nums.size());
        return m_res;
    }
};

【每日打卡】[leetcode+剑指offer] 169-多数元素

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/majority-element

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

这道题也是《剑指offer》原题——面试题39数组中出现次数超过一半的数字。

https://leetcode-cn.com/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/

一、题目描述

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

  • 输入: [3,2,3]
  • 输出: 3

示例 2:

  • 输入: [2,2,1,1,1,2,2]
  • 输出: 2

二、题解

2.1 哈希表

使用哈希表的思路:用数字作为key,每遇到一次,值加一。遍历完数字后,找出次数最大的。

思路很简单,时间复杂度O(N),空间复杂度O(N),N表示哈希表槽大小。

2.2 排序

因为元素在数组出现一半以上,所以排序后的中位数一定就是目标元素。

使用快速排序时间复杂度O(Nlog(N)),空间复杂度O(1)。

2.3 摩尔投票法

摩尔投票法的核心思想:因为元素出现次数超过一半,所以它出现的次数减去剩余元素出现次数一定是大于0的。基于这个思想,可以使用抵消的办法来消除掉所有非目标元素,然后剩下的就是目标元素了。

可以先随机找一个元素作为标杆,然后往后遍历,这个标杆元素每出现一次,次数加一。只要出现非标杆元素,次数减一。当次数变成0后,重新设置标杆元素,重复上面的过程。直到最后剩下来的元素就是目标元素。

以target表示目标元素,count表示出现的次数。过程描述:

array |[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
target| 7                | 5    | 5          | 7
count | 1, 2, 1, 2, 1, 0 | 1, 0 | 1, 2, 0, 0 | 1, 2, 3, 4

最后剩下来的目标元素是7,也就是我们需要的值。

时间复杂度O(N),空间复杂度0(1)。

三、代码

using namespace std;

class Solution {
public:
    int majorityElement(vector<int> &nums) {
        int count = 0, x;
        if (nums.empty()) {
            throw length_error("empty array");
        }

        x = nums[0];
        for (auto i : nums) {
            if (count == 0) {
                x = i;
            }
            if (x == i) {
                count++;
            } else {
                count--;
            }
        }

        return x;
    }
};

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/greatest-common-divisor-of-strings

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

一、题目描述

对于字符串 S 和 T,只有在 S = T + ... + T(T 与自身连接 1 次或多次)时,我们才认定 “T 能除尽 S”。

返回最长字符串 X,要求满足 X 能除尽 str1 且 X 能除尽 str2。

示例 1:

  • 输入:str1 = "ABCABC", str2 = "ABC"
  • 输出:"ABC"

示例 2:

  • 输入:str1 = "ABABAB", str2 = "ABAB"
  • 输出:"AB"

示例 3:

  • 输入:str1 = "LEET", str2 = "CODE"
  • 输出:""

提示:

  • 1 <= str1.length <= 1000
  • 1 <= str2.length <= 1000
  • str1[i] 和 str2[i] 为大写英文字母

二、题解

2.1 欧几里得算法

欧几里得算法(GCD)又称为辗转相除法,是用来计算两个整数的最大公因子的算法。

算法描述:

$$\begin{equation} gcd(a, b) = \begin{cases} a & b = 0\\ gcd(b, a \bmod b) & b \ne 0 \end{cases} \end{equation}$$

通过表达式能看出,算法是通过对两个数互相取模来计算得到最大公因子,也因此叫做辗转相除法。

算法依赖定理:两个整数的最大公约数等于其中较小的那个数和两数相除余数的最大公约数。

证明:

假设a=kb+r,四者皆为整树,r表示a%b的余数。假设m是a和b的最大公约数,那么可以推算得到:

a=kb+r  ==>  r=a-kb  ==>  r/m=a/m-kb/m;

最后一个表达式表示等式两边同时除以公约数,因为m是a和b的约数,所以a/m和kb/m都是整数,两者的差值也是整数。间接就说明r/m也是整数,一旦r/m是整数,那么m也是r的约数,即m也是a%b的余数的约数。证明了定理。

2.2 本题解析

在知晓了上面的定理后,就可以使用GCD算法来计算这个问题。只要对两个字符出的长度求出最大公约数即可。

问题的关键点在于两者是否存在最大公约数,假设用{}表示公约数,存在公约数的两个字符串格式一定是:

{}{}{}   {}{}

如果不存在公约数,两个字符串格式就是:

{}{}{}   }{ 或 }{} 

能看出的规律:如果存在公约数,str1+str2=str2+str1,因为两个字符串都是由公因子组成,相加起来只是多个公因子的组合而已。而如果不存在公约数,两者相加后一定不相等,因此基于这一点就能判断字符串是否有公因子了。

三、代码

class Solution {
public:
private:
    int gcd(int a, int b) {
        if (b == 0)
            return a;
        else
            return gcd(b, a % b);
    }
public:
    string gcdOfStrings(string str1, string str2) {
        if (str1 + str2 != str2 + str1)
            return "";

        return str1.substr(0, gcd(str1.size(), str2.size()));
    }
}

测试案例:

#include <gtest/gtest.h>
TEST(leetcode, demo) {
    Solution s;
    EXPECT_EQ("ABC", s.gcdOfStrings("ABCABC", "ABC"));
    EXPECT_EQ("AB", s.gcdOfStrings("ABABAB", "ABAB"));
    EXPECT_EQ("", s.gcdOfStrings("LEET", "CODE"));
}

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/partition-array-into-three-parts-with-equal-sum

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

一、题目描述

你一个整数数组 A,只有可以将其划分为三个和相等的非空部分时才返回 true,否则返回 false。

形式上,如果可以找出索引i+1 < j且满足(A[0] + A[1] + ... + A[i] == A[i+1] + A[i+2] + ... + A[j-1] == A[j] + A[j-1] + ... + A[A.length - 1])就可以将数组三等分。

示例1:

  • 输入:[0,2,1,-6,6,-7,9,1,2,0,1]
  • 输出:true
  • 解释:0 + 2 + 1 = -6 + 6 - 7 + 9 + 1 = 2 + 0 + 1

示例2:

  • 输入:[0,2,1,-6,6,7,9,-1,2,0,1]
  • 输出:false

示例3:

  • 输入:[3,3,6,5,-2,2,5,1,-9,4]
  • 输出:true
  • 解释:3 + 3 = 6 = 5 - 2 + 2 + 5 + 1 - 9 + 4

二、题目解析

要求三个相等的和,首先要知道的是每一段的和是多少,这就要先计算整个数组的总和。

  • 如果总和不能整除3,可以直接返回false,因为输入的都是整数,每一段的和也都是整数。
  • 如果总和能整除3,从头开始遍历数组,每找到一个满足条件的计数加1,直到最后如果计数为3的话就说明是存在符合条件的区间。

一个需要额外注意的地方是:

上面的数组中,总和是0,三个分区间的和也应该是0。但是实际上,按照上面的算法,计数为4,这个场景怎么处理?实际上,只要把最后一个区间归类到第三个区间就可以了,只要他们两者的和满足条件就行了。

而第三个区间是满足条件的,这种情况下,只要从第四个满足条件的区间开始,后面所有元素的总和不会对第三个区间的和造成影响的时候(即后面所有元素和为0),数组是满足条件的。

三、代码

class Solution {
public:
    bool canThreePartsEqualSum(vector<int> &A) {
        int i, cnt = 0, sum, tmp_sum, n;
        // 计算所有的总和
        for (auto x: A) {
            cnt += x;
        }

        // 如果总和不能整除3,直接返回false
        if (cnt % 3 != 0)
            return false;
        // 计算每一段的和
        sum = cnt / 3;

        // 找到三段符合条件的和
        tmp_sum = 0, n = 0;
        for (i = 0; i < A.size() && n < 3; i++) {
            tmp_sum += A[i];
            if (tmp_sum == sum) {
                // 每找到一段,tmp_sum置零,重新开始找
                n++;
                tmp_sum = 0;
            }
        }

        // 已经得到了三段满足条件的和,但是数组可能并没有遍历完
        // 只有在剩余元素的总和是0的条件下才认为满足条件
        tmp_sum = 0;
        for (; i < A.size(); i++) {
            tmp_sum += A[i];
        }

        return n == 3 && tmp_sum == 0;
    }
};

复杂度分析

  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

单元测试案例:

TEST(Solution, leetcode_1) {
    Solution s;
    vector<int> v{0, 2, 1, -6, 6, -7, 9, 1, 2, 0, 1};
    EXPECT_TRUE(s.canThreePartsEqualSum(v));
}

TEST(Solution, leetcode_2) {
    Solution s;
    vector<int> v{0, 2, 1, -6, 6, 7, 9, -1, 2, 0, 1};
    EXPECT_FALSE(s.canThreePartsEqualSum(v));
}

TEST(Solution, leetcode_3) {
    Solution s;
    vector<int> v{3, 3, 6, 5, -2, 2, 5, 1, -9, 4};
    EXPECT_TRUE(s.canThreePartsEqualSum(v));
}

TEST(Solution, leetcode_4) {
    Solution s;
    vector<int> v{10, -10, 10, -10, 10, -10, 10, -10};
    EXPECT_TRUE(s.canThreePartsEqualSum(v));
}

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/invert-binary-tree

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

一、题目描述

翻转一棵二叉树。

示例:

输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

输出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1

二、题解

简单题,有两种思路:递归和队列。

2.1 递归

每遍历到一个节点,调整左右子树的值。然后分别递归遍历左右子树。

TreeNode *invertTree(TreeNode *root) {
    TreeNode *p;

    if (root == nullptr) {
        return nullptr;
    }

    // 调整左右子树
    p = root->left;
    root->left = root->right;
    root->right = p;

    // 遍历左右子树
    invertTree(root->left);
    invertTree(root->right);

    return root;
}

2.2 使用队列

把根节点放到队列中,然后依次访问队列中的节点,把每个节点的左右子树位置对换,然后把左右子节点放到栈中继续遍历。

TreeNode *invertTree(TreeNode *root) {
    queue<TreeNode *> q;
    TreeNode *node, *tmp;

    if (root == nullptr) {
        return nullptr;
    }

    // 根节点入队
    q.push(root);
    while (!q.empty()) {
        // 取队首元素
        node = q.front();
        q.pop();

        // 调换左右子树
        tmp = node->left;
        node->left = node->right;
        node->right = tmp;

        // 左子树入队
        if (node->left) {
            q.push(node->left);
        }
        // 右子树入队
        if (node->right) {
            q.push(node->right);
        }
    }

    return root;
}

三、备注

这个问题是受到Max Howell 原问题启发的 :

谷歌:我们90%的工程师使用您编写的软件(Homebrew),但是您却无法在面试时在白板上写出翻转二叉树这道题,这太糟糕了。

其中的一个评论给也很有意思:

如果我是你,我会把白板从下到上翻转。然后告诉他:那,这就是我翻转的二叉树。

来源:力扣(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");
}

一、索引类型

索引根据底层实现可分为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对比整理得来。