2021-01-29

在软件开发的过程中,数据库设计是非常重要的,它需要根据需求分析设抽象出E-R图,逻辑结构设计,数据库选型,物理设计,实施及运维。下面就聊聊那些年数据库设计的那些事。

软件工程

图片

在问题定义和可行性分析都做好的前提下,就可以进入需求分析阶段了,通常来讲,一般都有产品部,需求分析往往都是由产品经理和客户去沟通落地形成PRD,跟开发沟通之后,就可以根据需求分析做数据库设计了,下面主要讨论下数据库设计的步骤以及每个阶段要完成的内容。

数据库设计基本步骤

图片

需求分析阶段

要进行数据库设计首先要了解用户需求,参与到用户需求分析中去,需求分析常用SA(Structured Analysis:结构化分析方法)强调开发方法的结构合理性以及所开发软件的结构合理性的软件开发方法,是生命周期法的继承与发展,是生命周期法与结构化程序设计思想的结合。

其基本思想是用系统工程的思想和工程化得方法,根据用户至上的原则,自始自终按照结构化、模块化,自顶向下地对系统进行分析与设计。建立的主要步骤如下:

  1. 首先画系统的输入输出,先画顶层数据流程图(DFD:Data Flow Diagram),顶层数据流程图只包含一个加工,用以表示被开发的系统,然后考虑该系统有哪些输入、输出数据流。

  2. 画系统内部,即画下层数据流层图。

下面是一个交易系统的DFD,需要先画出顶层数据流图,主要包括系统子模块之间的交互,然后再进一步对子模块进行分解。

图片

概念设计阶段

概念设计是整个数据库设计的关键,它是对需求分析阶段的成果进行综合,归档以及抽象出一个独立具体的DBMS模型,与具体的RDBMS产品无关。

在实际的开发中,常用E-R(Entity-Relationship:实体关系)图来表示,常用的工具PowerDesigner,可以实现CDM(概念数据模型)->LDM(逻辑数据模型)->PDM(物理数据模型)->Database的自动转换,这个过程称为正向工程,如果有database建库脚本,也可以通过PowerDesigner工具生成CDM,即Database->PDM->LDM->CDM,称为反向工程

图片概念设计通常采用自底向上,首先定义各系统局部的概念模型,然后再将他们集成合并起来,得到全局的概念模型。

举个例子说明下,现在负责交易系统的开发,主要涉及订单,价格模块,分别交给不同的开发去设计开发。

首先每个人要根据需求分析抽象出自己的实体Entity及之间的关系Relationship,设计初步完成之后就要开会讨论了,把每个开发的ER图合并起来,就得到全局交易系统的CDM。

图片

名词动词形容词分析法

开发如何根据需求分析设计ER图,完成模块的详细设计,提供接口文档,最重要的是需求分析抽象CDM阶段的ER图,一种行之有效的方法就是名称动词形容词分析法,下面就详细解释下这种分析方法。

还是举例说明吧,现在让我负责交易系统的订单这块的开发,在需求分析文档里看到一句话实现订单的高效管理,分析的过程如下:

名称:订单,订单就是一个Entity,也可以拆分成多个Entity,抽象出每个Entity的Attribute(前期可能由于需求不明确,可以只做确认的内容),Entity通过PowerDesigner的正向工程转换成数据库里的数据表,Attribute就是表的字段;数据表通过ORM映射到Java里的就是Class,字段就是private属性。

动词:管理,也就是要对订单要进行增删改查CRUD操作。

形容词:高效,首先想到在订单表上创建合适的索引吧,其次根据业务的发展,订单表太大会影响写入性能,是否要进行读写分离,分库分表操作。图片

数据库设计三范式

第一范式1NF:确保每个字段保持原子性,不可分割。

对于用户表users来说,有用户姓名(一般由first_name和last_name组成),如果使用类似Oracle的复合数据类型,就违反了1NF。

图片

很明显users的字段user_name是一个自定类型,是可分解的,这就违反了1NF。

第二范式2NF:确保字段完全依赖于主键。

一个表中只能保存一种数据,不可以把多种数据保存在一张表里,假如一张表既存储了用户信息,又存储商品信息,还存储了订单信息,这样就违反了2NF,而应该将用户表,商品表,订单表拆分成三张表,确保字段是该表拥有的。

第三范式3NF:必须满足2NF,实体中每个属性与主键直接相关而不能间接相关。

这个也不难理解,对于订单表orders来讲,是要存储用户表users的user_id,要明确哪个用户下的单,有些业务场景是要获取users表的用户姓名user_name,为了减少orders和users表的关联查询,将user_name冗余到orders表中,这种设计就违反了3NF,减少数据冗余,可以通过主外键进行表之间连接。

图片

到底该不该使用外键Foreign Key

外键目的是为了保证数据完整性和一致性,避免产生脏数据,设置外键有啥缺点呢。

  1. 影响写入性能:对于insert来说,每次都要判断从表的外键列是否在主表中存在(例如每次插入orders表,都要判断下user_id是否在users中存在),会降低数据库的写入性能,对于MySQL本来就只有Master输出写能力的数据库,就不太合适了,MySQL开发规范规定不允许使用外键也是有一定道理的。

  2. 并发问题:在使用外键的情况下,每次修改数据都需要去另外一个表检查数据,需要获取额外的锁。在高并发大场景,使用外键造成死锁或锁等几率更大。

实际开发中,更多的是不靠外键来保证数据的完整性和一致性,而是通过的业务逻辑,比如用户要下单,必须先登录系统,下单只需要将登录的用户编号写入到订单表,这个用户必然是存在于用户表的,对于一个用户友好的系统来说,尽量让用户选择,不要人工输入,这样可以保证数据一致性,避免脏数据的产生。

逻辑设计阶段

逻辑设计阶段是将概念数据模型转换为具体的DBMS所支持的数据模型,并将进行优化。虽然LDM独立于DBMS的,但可以进行外键,索引,视图等对象的设计工作。

在此阶段,各子模块的E-R图之间的冲突主要有三类:属性冲突,命名冲突和结构冲突,同时E-R图向关系模型的转换,要解决如何将实体性和实体间的联系转换为关系模式,确定这些关系模式的属性和码,实际开发中,逻辑设计阶段不是必须的,有些是从CDM直接到PDM了。

图片

数据库选型

数据库选型是非常重要的环节,一般在需求分析完成之后,通过架构评审会进行确认,数据库方面主要包括数据存储,检索,安全,读写分离,分库分表,数据归档,接入数据仓库都要进行确认,根据业务的场景对相关的数据库产品进行调研比对,选择最适合业务场景的数据库作为存储。

举个例子:对于一个DAU 1000W  TPS 3W的交易的业务场景,如果使用MySQL来存储,我们知道原生的MySQL写入瓶颈,以及订单相关表数据量增长过快导致的性能问题,不太适合这种高并发写的场景,可以考虑使用分布式MySQL,例如常见的DRDS,TiDB,OceanBase。既可以解决原生MySQL写入瓶颈,同时也可以处理单表数据量大导致的分库分表问题。

同样对于优酷,爱奇艺这种视频类系统,使用MySQL来存储就不太合适了,应该采用MongoDB集群来存储;对于京东,淘宝的这种搜索服务采用ElastSearch数据库集群处理会更高效。

物理设计阶段

逻辑设计阶段和数据库选型完成之后,就可以通过LDM生成PDM了,在物理设计阶段,需要设计跟RDBMS相关的对象,例如设计存储过程,触发器,用户自定义函数,表空间等。

图片

数据库实施阶段

图片

例如选择的是MySQL数据库,通过PDM生成数据库的建库脚本之后,需要进行规范性检查,通过之后就可以创建表结构,规范性检查可以借助开源的SQL审核工具,如Yearning,Archery都可以设置规则,检查之后会给出整改建议,能够帮我们自动实现SQL Review。Yearning是用go开发,目前只支持MySQL数据库,Archery可以支持多种数据库。下面是Yearning自动化SQL审核平台的一个DDL工单的检测示例。

图片

检测通过后就可以提交工单了,审核通过后就会自动执行DDL脚本建库。

数据库维护阶段

数据库维护阶段主要包括业务支撑和数据库运维,简单总结了下,如下图所示。

图片

总结

实际开发中,数据库设计阶段是非常重要,通常都是开发自己根据业务模块的需求去分析,抽取成CDM中的E-R图,转换成LDM,经过数据库选型及生成PDM,最终生成数据库表,然后才能开始coding,测试、发布上线以及版本迭代,为了保证线上业务的安全稳定高效,就需要对数据库进行精细化管理和维护。

仔细观察不难发现,数据库设计的核心就是对需求分析的理解以及抽取沉底出E-R图,这就需要对行业及相关业务有深刻立即及抽象能力,大家有木有发现,招聘Java工程师的前面附加了业务属性,例如用户域Java工程师,支付域Java工程师,主要体现在需求分析抽象以及数据模型设计能力上,开发过程中多参与业务需求讨论是非常有必要的。今天就聊这么多,希望对大家有所帮助。

  • 0
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个可能的Java实现: ```java import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; public class RentPlanGenerator { private static final double RENT_INCREASE_RATE = 0.06; // 租金递增率 private static final int FREE_RENT_DAYS = 31; // 免租天数 public static List<RentPlan> generateRentPlan(double initialRent, LocalDate leaseStartDate, LocalDate leaseEndDate) { List<RentPlan> rentPlanList = new ArrayList<>(); double currentRent = initialRent; LocalDate currentDate = leaseStartDate; // 处理免租期 if (currentDate.isBefore(leaseStartDate.plusDays(FREE_RENT_DAYS))) { currentDate = leaseStartDate.plusDays(FREE_RENT_DAYS); } while (currentDate.isBefore(leaseEndDate)) { LocalDate nextIncreaseDate = currentDate.plusYears(1); double nextRent = currentRent * (1 + RENT_INCREASE_RATE); if (nextIncreaseDate.isBefore(leaseStartDate.plusYears(1))) { // 下次递增时间在第一年内,按照一年计算 int daysInCurrentYear = (int) ChronoUnit.DAYS.between(currentDate, nextIncreaseDate); rentPlanList.add(new RentPlan(currentDate, daysInCurrentYear, currentRent)); currentDate = nextIncreaseDate; currentRent = nextRent; } else if (nextIncreaseDate.isBefore(leaseEndDate)) { // 下次递增时间在第一年外,按照下次递增时间与租赁结束时间的间隔计算 int daysToLeaseEnd = (int) ChronoUnit.DAYS.between(currentDate, leaseEndDate); rentPlanList.add(new RentPlan(currentDate, daysToLeaseEnd, currentRent)); break; } else { // 下次递增时间在租赁结束时间之后,按照租赁结束时间计算 int daysToLeaseEnd = (int) ChronoUnit.DAYS.between(currentDate, leaseEndDate); rentPlanList.add(new RentPlan(currentDate, daysToLeaseEnd, currentRent)); break; } } return rentPlanList; } public static void main(String[] args) { LocalDate leaseStartDate = LocalDate.of(2021, 3, 1); LocalDate leaseEndDate = LocalDate.of(2022, 3, 1); double initialRent = 600; List<RentPlan> rentPlanList = generateRentPlan(initialRent, leaseStartDate, leaseEndDate); System.out.printf("%-12s%-12s%-12s%n", "时间", "天数", "租金"); for (RentPlan rentPlan : rentPlanList) { System.out.printf("%-12s%-12d%-12.2f%n", rentPlan.getStartDate(), rentPlan.getDays(), rentPlan.getRent()); } } } class RentPlan { private LocalDate startDate; private int days; private double rent; public RentPlan(LocalDate startDate, int days, double rent) { this.startDate = startDate; this.days = days; this.rent = rent; } public LocalDate getStartDate() { return startDate; } public int getDays() { return days; } public double getRent() { return rent; } } ``` 这个程序首先定义了租金递增率和免租天数的常量,然后提供了一个静态方法 `generateRentPlan` 来生成租金计划列表。该方法接受三个参数:初始月租金、租赁开始时间和租赁结束时间。 具体实现时,我们使用循环来逐月生成租金计划。在每次循环中,我们首先计算下次递增租金的时间和金额。然后根据下次递增时间与租赁开始时间的间隔,决定本次循环处理的天数和租金金额。最后将这些信息保存到一个 `RentPlan` 对象中,并添加到租金计划列表中。 在主函数中,我们使用 `generateRentPlan` 方法生成租金计划列表,并以表格形式输出。输出结果如下: ``` 时间 天数 租金 2021-04-01 30 600.00 2021-05-01 31 636.00 2021-06-01 30 674.16 2021-07-01 31 713.57 2021-08-01 31 754.29 2021-09-01 30 796.39 2021-10-01 31 840.94 2021-11-01 30 887.02 2021-12-01 31 934.72 2022-01-01 31 984.12 2022-02-01 28 1035.30 ``` 可以看到,程序正确地根据递增周期和递增率生成了每个月的租金计划,并且考虑了免租期的影响。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值