<audio id="audio" title="28 | 手把手带你设计一个完整的连锁超市信息系统数据库（下）" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f4/91/f4c85c085e494f53dac71ca3105fc691.mp3"></audio>

你好，我是朱晓峰。

上节课，我们完成了项目的需求分析和业务流程的梳理，为设计数据库做好了准备工作，接下来我们就可以开始具体的设计了。所以，今天，我就带你来建库建表、创建外键约束、视图、存储过程和触发器，最后制定容灾和备份的策略，从而完成一个完整的连锁超市项目数据库的设计，帮助你提高设计高效可靠的数据库的能力。

首先，我们一起来创建数据库和数据表。

## 如何创建数据库和数据表？

经过上节课的分库分表操作，我们把数据库按照业务模块，拆分成了多个数据库。其中，盘点模块中的数据表分别被拆分到了营运数据库（operation）和库存数据库（inventory）中。

下面我们就按照上节课的分库策略，分别创建营运数据库和库存数据库：

```
mysql&gt; CREATE DATABASE operation;
Query OK, 1 row affected (0.03 sec)
 
mysql&gt; CREATE DATABASE inventory;
Query OK, 1 row affected (0.02 sec)

```

接下来，我们来分别创建下这两个数据库中的表。

商户表、门店表、员工表、商品常用信息表和商品不常用信息表从属于营运数据库，我们先把这5个表创建出来。

商户表（operation.enterprice）：

```
mysql&gt; CREATE TABLE operation.enterprice
-&gt; (
-&gt; groupnumber SMALLINT PRIMARY KEY,  -- 组号
-&gt; groupname VARCHAR(100) NOT NULL,   -- 名称
-&gt; address TEXT NOT NULL,             -- 地址
-&gt; phone VARCHAR(20) NOT NULL,        -- 电话
-&gt; contactor VARCHAR(50) NOT NULL     -- 联系人
-&gt; );
Query OK, 0 rows affected (0.05 sec)

```

门店表（operation.branch）：

```
mysql&gt; CREATE TABLE operation.branch
-&gt; (
-&gt; branchid SMALLINT PRIMARY KEY,        -- 门店编号
-&gt; groupnumber SMALLINT NOT NULL,        -- 组号
-&gt; branchname VARCHAR(100) NOT NULL,     -- 门店名称
-&gt; address TEXT NOT NULL,                -- 地址
-&gt; phone VARCHAR(20) NOT NULL,           -- 电话
-&gt; branchtype VARCHAR(20) NOT NULL,      -- 门店类别
-&gt; CONSTRAINT fk_branch_enterprice FOREIGN KEY (groupnumber) REFERENCES operation.enterprice(groupnumber)  -- 外键约束，组号是外键
-&gt; );
Query OK, 0 rows affected (0.07 sec)

```

员工表（operation.employee）：

```
mysql&gt; CREATE TABLE operation.employee
-&gt; (
-&gt; employeeid SMALLINT PRIMARY KEY,     -- 员工编号
-&gt; groupnumber SMALLINT NOT NULL,       -- 组号
-&gt; branchid SMALLINT NOT NULL,          -- 门店编号
-&gt; workno VARCHAR(20) NOT NULL,         -- 工号
-&gt; employeename VARCHAR(100) NOT NULL,  -- 员工名称
-&gt; pid VARCHAR(20) NOT NULL,            -- 身份证
-&gt; address VARCHAR(100) NOT NULL,       -- 地址
-&gt; phone VARCHAR(20) NOT NULL,          -- 电话
-&gt; employeeduty VARCHAR(20) NOT NULL,   -- 职责
-&gt; CONSTRAINT fk_employee_branch FOREIGN KEY (branchid) REFERENCES operation.branch(branchid)
-&gt; );
Query OK, 0 rows affected (0.07 sec)

```

商品常用信息表（operation.goods_o）：

```
mysql&gt; CREATE TABLE operation.goods_o
-&gt; (
-&gt; itemnumber MEDIUMINT PRIMARY KEY,   -- 商品编号
-&gt; groupnumber SMALLINT NOT NULL,      -- 组号
-&gt; barcode VARCHAR(50) NOT NULL,       -- 条码
-&gt; goodsname TEXT NOT NULL,            -- 名称
-&gt; salesprice DECIMAL(10,2) NOT NULL,  -- 售价
-&gt; PRIMARY KEY (groupnumber,itemnumber)-- 主键 
-&gt; );
Query OK, 0 rows affected (0.06 sec)

```

商品不常用信息表（operation.goods_f）：

```
mysql&gt; CREATE TABLE operation.goods_f
-&gt; (
-&gt; itemnumber MEDIUMINT PRIMARY KEY,       -- 商品编号
-&gt; groupnumber SMALLINT NOT NULL,          -- 组号
-&gt; specification TEXT NOT NULL,            -- 规格
-&gt; unit VARCHAR(20) NOT NULL,              -- 单位
-&gt; PRIMARY KEY (groupnumber,itemnumber)    -- 主键
-&gt; );
Query OK, 0 rows affected (0.06 sec)

```

好了，现在我们来创建库存数据库中的表。仓库表、库存表、盘点单头表、盘点单明细表、盘点单头历史表和盘点单明细历史表，从属于库存数据库。

仓库表（inventory.stockmaster）：

```
mysql&gt; CREATE TABLE inventory.stockmaster
-&gt; (
-&gt; stockid SMALLINT PRIMARY KEY,     -- 仓库编号
-&gt; groupnumber SMALLINT NOT NULL,    -- 组号
-&gt; branchid SMALLINT NOT NULL,       -- 门店编号
-&gt; stockname VARCHAR(100) NOT NULL,  -- 仓库名称
-&gt; stockkind VARCHAR(20) NOT NULL,   -- 仓库种类
-&gt; CONSTRAINT fk_stock_branch FOREIGN KEY (branchid) REFERENCES operation.branch(branchid)           -- 外键约束，门店编号是外键
-&gt; );
Query OK, 0 rows affected (0.07 sec)

```

库存表（inventory.inventory）：

```
mysql&gt; CREATE TABLE inventory.inventory
-&gt; (
-&gt; id INT PRIMARY KEY AUTO_INCREMENT,       -- 库存编号
-&gt; groupnumber SMALLINT NOT NULL,           -- 组号
-&gt; branchid SMALLINT NOT NULL,              -- 门店编号
-&gt; stockid SMALLINT NOT NULL,               -- 仓库编号
-&gt; itemnumber MEDIUMINT NOT NULL,           -- 商品编号
-&gt; itemquantity DECIMAL(10,3) NOT NULL      -- 商品数量
-&gt; );
Query OK, 0 rows affected (0.06 sec)

```

盘点单头表（inventory.invcounthead）：

```
mysql&gt; CREATE TABLE inventory.invcounthead
-&gt; (
-&gt; listnumber INT PRIMARY KEY,              -- 单号
-&gt; groupnumber SMALLINT NOT NULL,           -- 组号
-&gt; branchid SMALLINT NOT NULL,              -- 门店编号
-&gt; stockid SMALLINT NOT NULL,               -- 仓库编号
-&gt; recorder SMALLINT NOT NULL,              -- 录入人编号
-&gt; recordingdate DATETIME NOT NULL          -- 录入时间
-&gt; );
Query OK, 0 rows affected (0.07 sec)

```

盘点单明细表（inventorry.invcountdetails）：

```
mysql&gt; CREATE TABLE inventory.invcountdetails
-&gt; (
-&gt; id INT PRIMARY KEY AUTO_INCREMENT,   -- 明细编号
-&gt; listnumber INT NOT NULL,             -- 单号
-&gt; groupnumber SMALLINT NOT NULL,       -- 组号
-&gt; branchid SMALLINT NOT NULL,          -- 门店编号
-&gt; stockid SMALLINT NOT NULL,           -- 仓库编号
-&gt; itemnumber MEDIUMINT NOT NULL,       -- 商品编号
-&gt; accquant DECIMAL(10,3) NOT NULL,     -- 结存数量
-&gt; invquant DECIMAL(10,3) NOT NULL,     -- 盘存数量
-&gt; plquant DECIMAL(10,3) NOT NULL       -- 盈亏数量
-&gt; );
 
Query OK, 0 rows affected (0.07 sec)

```

盘点单头历史表（inventory.invcountheadhist）：

```
mysql&gt; CREATE TABLE inventory.invcountheadhist
-&gt; (
-&gt; listnumber INT PRIMARY KEY,       -- 单号
-&gt; groupnumber SMALLINT NOT NULL,    -- 组号
-&gt; branchid SMALLINT NOT NULL,       -- 门店编号
-&gt; stockid SMALLINT NOT NULL,        -- 仓库编号
-&gt; recorder SMALLINT NOT NULL,       -- 录入人编号
-&gt; recordingdate DATETIME NOT NULL,  -- 录入时间
-&gt; confirmer SMALLINT NOT NULL,      -- 验收人编号
-&gt; confirmationdate DATETIME NOT NULL -- 验收时间
-&gt; );
Query OK, 0 rows affected (0.10 sec)

```

盘点单明细历史表（inventorry.invcountdetailshist）：

```
mysql&gt; CREATE TABLE inventory.invcountdetailshist
-&gt; (
-&gt; id INT PRIMARY KEY AUTO_INCREMENT,   -- 明细编号
-&gt; listnumber INT NOT NULL,             -- 单号
-&gt; groupnumber SMALLINT NOT NULL,       -- 组号
-&gt; branchid SMALLINT NOT NULL,          -- 门店编号
-&gt; stockid SMALLINT NOT NULL,           -- 仓库编号
-&gt; itemnumber MEDIUMINT NOT NULL,       -- 商品编号
-&gt; accquant DECIMAL(10,3) NOT NULL,     -- 结存数量
-&gt; invquant DECIMAL(10,3) NOT NULL,     -- 盘存数量
-&gt; plquant DECIMAL(10,3) NOT NULL       -- 盈亏数量
-&gt; );
Query OK, 0 rows affected (1.62 sec)

```

至此，我们完成了创建数据库和数据表的工作。为了提高查询的速度，我们还要为数据表创建索引。下面我们就来实际操作一下。

### 如何创建索引？

索引对提升数据查询的效率作用最大，没有之一。我们创建索引的策略是：

1. 所有的数据表都必须创建索引；
1. 只要是有可能成为查询筛选条件的字段，都必须创建索引。

这样做的原因是，当数据量特别大的时候，如果没有索引，一旦出现大并发，没有索引的表很可能会成为访问的瓶颈。而且这个问题十分隐蔽，不容易察觉，系统也不会报错，但是却会消耗大量的CPU资源，导致系统事实上的崩溃。

在之前的操作中，我们一共创建了11个数据表，下面我们就来一一为这些表创建索引。

商户表中的组号字段，常被用于筛选条件。我们用商户表的组号字段为商户表创建索引，代码如下所示：

```
mysql&gt; CREATE INDEX index_enterprice_groupname ON operation.enterprice (groupname);
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0

```

门店表的组号字段也常被用作筛选条件，所以，我们用组号字段为门店表创建索引，代码如下所示：

```
mysql&gt; CREATE INDEX index_branch_groupnumber ON operation.branch (groupnumber);
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

除了组号字段，门店名称字段也常用在查询的筛选条件中，下面我们就用门店名称字段为门店表创建索引：

```
mysql&gt; CREATE INDEX index_branch_branchname ON operation.branch (branchname);
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

门店类别字段也是用作筛选条件的字段之一，我们可以用门店类别字段为门店表创建索引，如下所示：

```
mysql&gt; CREATE INDEX index_branch_branchtype ON operation.branch (branchtype);
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

在员工表中，组号、门店编号、身份证号、电话和职责字段常被用作查询的筛选条件，下面我们就分别用这几个字段为员工表创建索引。

第一步，用组号字段为员工表创建索引：

```
mysql&gt; CREATE INDEX index_employee_groupnumer ON operation.employee (groupnumber);
Query OK, 0 rows affected (0.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

第二步，用门店编号字段为员工表创建索引：

```
mysql&gt; CREATE INDEX index_employee_branchid ON operation.employee (branchid);
Query OK, 0 rows affected (0.07 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

第三步，用身份证字段为员工表创建索引：

```
mysql&gt; CREATE INDEX index_employee_pid ON operation.employee (pid);
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

第四步，用电话字段为员工表创建索引：

```
mysql&gt; CREATE INDEX index_employee_phone ON operation.employee (phone);
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

最后，我们用职责字段为员工表创建索引：

```
mysql&gt; CREATE INDEX index_employee_duty ON operation.employee (employeeduty);
Query OK, 0 rows affected (0.09 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

对于商品常用信息表（operation.goods_o），我们发现，组号和售价字段常被用在查询筛选条件语句中，所以，我们分别用这两个字段为商品常用信息表创建索引。

首先，用组号字段为商品常用信息表创建索引，如下所示：

```
mysql&gt; CREATE INDEX index_goodso_groupnumber ON operation.goods_o (groupnumber);
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

然后，用价格字段为商品常用信息表创建索引，如下所示：

```
mysql&gt; CREATE INDEX index_goodso_salesprice ON operation.goods_o (salesprice);
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

对于商品不常用信息表，只有组号字段经常用在查询的筛选条件中，所以，我们只需要用组号字段为商品不常用信息表创建索引。代码如下：

```
mysql&gt; CREATE INDEX index_goodsf_groupnumber ON operation.goods_f (groupnumber);
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

到这里，我们就完成了为营运数据库中的表创建索引的工作，下面我们来为库存数据库中的表创建索引。

首先是仓库表。这个表中经常被用做筛选条件的字段，是组号和门店编号字段。

我们先用组号字段为仓库表创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_stock_groupnumber ON inventory.stockmaster (groupnumber);
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

然后我们用门店编号字段为仓库表创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_stock_branchid ON inventory.stockmaster (branchid);
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

接下来，我们为库存表创建索引。库存表中常用于筛选的字段有组号、门店编号和商品编号字段。

我们先用组号字段来创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_inventory_groupnumber ON inventory.inventory (groupnumber);
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

然后，我们用门店编号字段创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_inventory_branchid ON inventory.inventory (branchid);
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

最后用商品编号字段创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_inventory_itemnumber ON inventory.inventory (itemnumber);
Query OK, 0 rows affected (0.07 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

盘点单头表也需要创建索引，常用的筛选字段是门店编号。那么，我们就用门店编号为盘点单头表创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_invcounthead_branchid ON inventory.invcounthead (branchid);
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

盘点单头明细表中常用于筛选的字段有门店编号和商品编号，我们分别用这2个字段创建索引。

首先是用门店编号字段创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_invcountdetails_branchid ON inventory.invcountdetails (branchid);
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

然后是用商品编号字段创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_invcountdetails_itemnumber ON inventory.invcountdetails (itemnumber);
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

盘点单头历史表数据量比较大，主要用于查询，常用的筛选字段有门店编号和验收时间。我们先用门店编号字段创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_invcounthaedhist_branchid ON inventory.invcountheadhist (branchid);
Query OK, 0 rows affected (0.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

然后用验收时间字段创建索引，代码如下：

```
mysql&gt; CREATE INDEX index_invcounthaedhist_confirmationdate ON inventory.invcountheadhist (confirmationdate);
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

盘点单明细历史表是整个盘点模块中数据量最大的表，主要用于查询，索引对提升查询效率来说非常重要。要是忘了创建索引，很可能成为整个系统的瓶颈。

这个表中用作筛选条件的字段主要有门店编号和商品编号，我们分别用它们创建索引。首先是门店编号字段，创建索引的代码如下：

```
mysql&gt; CREATE INDEX index_invcountdetailshist_branchid ON inventory.invcountdetailshist (branchid);
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

然后是商品编号字段，创建索引的代码如下：

```
mysql&gt; CREATE INDEX index_invcountdetailshist_itemnumber ON inventory.invcountdetailshist (itemnumber);
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

```

到这里，索引就全部创建完成了。

由于我们把盘点单拆分成了单头和明细两个表，在应用中，经常要用到单头和明细的全部信息，所以，为了使代码更加简洁，查询更加方便，我们要为这两个表创建视图。

### 如何创建视图？

首先，我们为盘点单创建视图，代码如下：

```
mysql&gt; CREATE VIEW view_invcount
    -&gt; AS
    -&gt; SELECT a.*,b.itemnumber,b.accquant,b.invquant,b.plquant
    -&gt; FROM inventory.invcounthead AS a
    -&gt; JOIN
    -&gt; inventory.invcountdetails AS b
    -&gt; ON (a.listnumber=b.listnumber);
Query OK, 0 rows affected (0.04 sec)

```

然后，我们为盘点单历史表创建视图，代码如下：

```
mysql&gt; CREATE VIEW view_invcounthist
    -&gt; AS
    -&gt; SELECT a.*,b.itemnumber,b.accquant,b.invquant,b.plquant
    -&gt; FROM inventory.invcountheadhist AS a
    -&gt; JOIN inventory.invcountdetailshist AS b
    -&gt; ON (a.listnumber=b.listnumber);
Query OK, 0 rows affected (0.02 sec)

```

### 如何创建存储过程？

在整个盘点模块中，有一个核心的计算模块，就是盘点单验收模块。这个计算模块，每次盘点结束都会被调用。为了提升执行效率，让代码更加模块化，使代码的可读性更好，我们可以把盘点表验收这个模块的数据处理逻辑，用存储过程的方式保存在服务器上，以方便应用程序进行调用。

下面我具体介绍存储过程的入口参数和数据处理逻辑。

存储过程的入口参数是单号和验收人的员工编号。存储过程的数据处理逻辑是：先用盈亏数量调整库存，计算方式是新库存 = 老库存 + 盈亏数量；然后把盘点单数据移到盘点单历史中去。

- 把盘点单明细表中的数据插入到盘点单明细历史表中；
- 把盘点单头表中的数据，插入到盘点单头历史表中；
- 删除盘点单明细表中的数据；
- 删除盘点单头表中的数据。

按照这个参数定义和计算逻辑，我们就可以用下面的代码来创建存储过程了：

```
CREATE DEFINER=`root`@`localhost` PROCEDURE `invcountconfirm`(mylistnumber INT,myconfirmer SMALLINT)
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE mybranchid INT;
DECLARE myitemnumber INT;
DECLARE myplquant DECIMAL(10,3);
DECLARE cursor_invcount CURSOR FOR 
SELECT branchid,itemnumber,plquant
FROM inventory.invcountdetails
WHERE listnumber = mylistnumber;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK; 

START TRANSACTION;         
 OPEN cursor_invcount; -- 打开游标
 FETCH cursor_invcount INTO mybranchid,myitemnumber,myplquant; -- 读入第一条记录
REPEAT
  UPDATE inventory.inventory
  SET itemquantity = itemquantity + myplquant    -- 更新库存
        WHERE itemnumber = myitemnumber
        AND branchid = mybranchid; 
  FETCH cursor_invcount INTO mybranchid,myitemnumber,myplquant; -- 读入下一条记录
 UNTIL done END REPEAT;
    
 CLOSE cursor_invcount;
    
  INSERT INTO inventory.invcountdetailshist 
(listnumber,groupnumber,branchid,stockid,itemnumber,accquant,invquant,plquant) 
  SELECT listnumber,groupnumber,branchid,stockid,itemnumber,accquant,invquant,plquant
  FROM inventory.invcountdetails 
  WHERE listnumber=mylistnumber;  -- 把这一单的盘点单明细插入历史表

  INSERT INTO inventory.invcountheadhist  
(listnumber,groupnumber,branchid,stockid,recorder,recordingdate,confirmer,confirmationdate) 
  SELECT listnumber,groupnumber,branchid,stockid,recorder,recordingdate,myconfirmer,now()
  FROM inventory.invcounthead
  WHERE listnumber=mylistnumber;  -- 把这一单的盘点单头插入历史 
  DELETE FROM inventory.invcountdetails WHERE listnumber = mylistnumber; -- 删除这一单的盘点单明细表数据
 DELETE FROM inventory.invcounthead WHERE listnumber = mylistnumber; -- 删除这一单的盘点单头表数据
COMMIT;
END

```

具体的操作我都标注在代码里面了，你可以看一下。

### 如何创建触发器？

创建完了存储过程，我们已经完成了一大半，但是别急，还有一步工作没有做，就是创建触发器。

由于我们根据分库分表的策略，把商品信息表拆分成了商品常用信息表和商品不常用信息表，这样就很容易产生数据不一致的情况。为了确保商品常用信息表和商品不常用信息表中的数据保持一致，我们可以创建触发器，保证这2个表中删除其中一个表的一条记录的操作，自动触发删除另外一个表中对应的记录的操作。这样一来，就防止了一个表中的记录在另外一个表中不存在的情况，也就确保了数据的一致性。

创建触发器的代码如下所示：

```
DELIMITER //
CREATE TRIGGER operation.del_goodso BEFORE DELETE  -- 在删除前触发
ON operation.goods_o
FOR EACH ROW                              -- 表示每删除一条记录，触发一次
BEGIN                                     -- 开始程序体
 DELETE FROM operation.goods_f
    WHERE groupnumber=OLD.groupnumber
    AND itemnumber=OLD.itemnumber;
END
//
DELIMITER ;

DELIMITER //
CREATE TRIGGER operation.del_goodsf BEFORE DELETE  -- 在删除前触发
ON operation.goods_f
FOR EACH ROW                              -- 表示每删除一条记录，触发一次
BEGIN                                     -- 开始程序体
 DELETE FROM operation.goods_o
    WHERE groupnumber=OLD.groupnumber
    AND itemnumber=OLD.itemnumber;
END
//
DELIMITER ;

```

到这儿呢，数据库数据表以及相关的索引、存储过程和触发器就都创建完了。可以说，我们已经完成了数据库的设计。

但是，在实际工作中，如果你只进行到这一步就打住了，那就还不能算是一个优秀的开发者。因为你考虑问题还不够全面。一个合格的系统设计者，不但要准确把握客户的需求，预见项目实施的前景，还要准备好对任何可能意外的应对方案。实际做项目时，不是纸上谈兵，什么情况都可能发生，我们需要未雨绸缪。所以，下面我们就来设计下项目的容灾和备份策略。

## 如何制定容灾和备份策略？

为了防止灾害出现，我设置了主从架构。为了方便你理解，我采用的是一主一从的架构，你也可以搭建一主多从的架构，原理都是一样的。

主从架构的核心是，从服务器实时自动同步主服务器的数据，一旦主服务器宕机，可以切换到从服务器继续使用。这样就可以把灾害损失降到最低。

下面我就和你一起，搭建一下主从服务器。

### 如何搭建主从服务器？

第一步，确保从服务器可以访问主服务器（在同一网段），例如，可以把主服务器的IP地址设置为：

```
主服务器IP：192.168.1.100

```

需要注意的是，主服务器入口方向的3306号端口需要打开，否则从服务器无法访问主服务器的MySQL服务器。

同时，我们把从服务器的IP地址设置为：

```
从服务器IP： 192.168.1.110

```

第二步，修改主从服务器的系统配置文件my.ini，使主从服务器有不同的ID编号，并且指定需要同步的数据库。

在主服务器的配置文件中，我们把主服务器的编号修改为：server-id = 1。

```
# ***** Group Replication Related *****
# Specifies the base name to use for binary log files. With binary logging
# enabled, the server logs all statements that change data to the binary
# log, which is used for backup and replication.
log-bin=mysql-bin          -- 二进制日志名称
binlog-do-db = operation   -- 需要同步的数据库：营运数据库
binlog-do-db = inventory   -- 需要同步的数据库：库存数据库

# ***** Group Replication Related *****
# Specifies the server ID. For servers that are used in a replication topology,
# you must specify a unique server ID for each replication server, in the
# range from 1 to 2^32 − 1. “Unique” means that each ID must be different
# from every other ID in use by any other source or replica.
server-id=1          -- 主服务器的ID设为1


```

然后，我们来修改从服务器的配置文件my.ini，把从服务器的编号设置为server-id = 2。

```
# ***** Group Replication Related *****
# Specifies the base name to use for binary log files. With binary logging
# enabled, the server logs all statements that change data to the binary
# log, which is used for backup and replication.
log-bin=mysql-bin              -- 二进制日志名称
replicate_do_db = operation    -- 需要同步过来的数据库：营运数据库
replicate_do_db = inventory    -- 需要同步过来的数据库：库存数据库

# ***** Group Replication Related *****
# Specifies the server ID. For servers that are used in a replication topology,
# you must specify a unique server ID for each replication server, in the
# range from 1 to 2^32 − 1. “Unique” means that each ID must be different
# from every other ID in use by any other source or replica.
server-id=2                       -- 从服务器的编号为2

```

第三步，在主从服务器上都保存配置文件，然后分别重启主从服务器上的MySQL服务器。

第四步，为了使从服务器可以访问主服务器，在主服务器上创建数据同步用户，并赋予所有权限。这样，从服务器就可以实时读取主服务器的数据了。

```
mysql&gt; CREATE USER 'myreplica'@'%' IDENTIFIED BY 'mysql';
Query OK, 0 rows affected (0.02 sec)

mysql&gt; GRANT ALL ON *.* TO 'myreplica'@'%';
Query OK, 0 rows affected (0.99 sec)

```

第五步，在从服务器上启动数据同步，开始从主服务器中同步数据。

```
mysql&gt;change master to master_host='192.168.1.100',master_port=3306,master_user='myreplica',master_password='mysql’,master_log_file='mysql-bin.000001',master_log_pos=535;
Query OK, 0 rows affected (0.02 sec)

```

启动同步的时候，你需要注意的是，必须指明主服务器上二进制日志中的位置master_log_pos。也就是说，你准备从主服务器的二进制日志的哪个位置开始同步数据。你可以通过在主服务器上，用SQL语句“SHOW BINLOG EVENTS IN 二进制日志名” 获取这个值。下面的代码可以启动同步：

```
mysql&gt;start slave;
Query OK, 0 rows affected (0.02 sec)

```

### 如何制定数据备份策略？

设置了主从服务器，也不是万无一失。

我曾经就遇到过这样一件事：我们把主从服务器搭在了某大厂几台不同的云服务器上，自以为没问题了，没想到大厂也有失手的时候，居然整个地区全部宕机，导致我们的主从服务器同时无法使用，近千家商户无法开展业务，损失惨重。

所以，无论系统的架构多么可靠，我们也不能大意。备份仍然是必不可少的步骤。我们可以在应用层面调用类似下面的命令进行备份：

```
H:\&gt;mysqldump -u root -p --databases
inventory operation &gt; H:\backup\Monday\mybackup.sql

```

我在项目中设定的策略是，每天晚上12:00做一个自动备份，循环备份7天，创建7个文件夹，从Monday到Sunday，每个文件夹中保存对应的备份文件，新的覆盖旧的。

这个逻辑也很简单，你很容易理解，我就不多解释了，你不要忘了做这一步工作就可以了。

## 总结

今天这节课，我给你详细讲解了建库建表、创建索引、存储过程、触发器，以及容灾和备份策略。有几点你需要格外重视一下。

索引是提升查询执行速度的关键，创建的原则是：所有的数据表都要创建索引；有可能作为筛选条件的字段，都要用来创建索引。

另外，容灾和备份是数据库系统设计中必不可少的部分。因为在现实生活中，什么情况都可能发生，我们无法预见，但是可以尽量避免。在设计阶段的未雨绸缪，可以帮助我们减少很多损失。

最后我要提醒你的是，MySQL的相关知识实践性非常强，决不能停留在纸面上。我在课中演示的的代码，都是在实际环境中运行过的，你课下一定要跟着实际操作一下。毕竟，学习知识最好的办法，就是在解决实际问题中学习。

## 思考题

在今天的课程中，我演示了搭建主从服务器的过程。其中，在第四步，我专门创建了一个用来同步数据的账号“myreplica”。我想请你思考一下，我为什么要这样做？直接用“root”账号不行吗？

欢迎在留言区写下你的思考和答案，我们一起交流讨论。如果你觉得今天的内容对你有所帮助，也欢迎你把它分享给你的朋友或同事，我们下节课见。
