1. 局部数据库+缓存
1.1. 如何避免单点故障?(高可用设计)
只要题目提到"避免单点故障"或"高可靠性",标准答案只有一套组合拳:
-
冗余(Redundancy):一台不够就两台。
-
热备(Hot Standby)/ 集群:主节点挂了,备节点立马顶上。
-
分布式/分片:鸡蛋不要放在一个篮子里。
本题答案套路:建立主从复制(Master-Slave)或热备份机制。局部数据库负责写,并同步给备份库;缓存负责读。
1.2. 数据的增删改查(CRUD)如何实现?(缓存策略)
这是经典的Cache-Aside Pattern(旁路缓存模式)。你必须背下来这套标准流程,软考只要考到缓存,90%都是这个逻辑。
-
读取(Read)逻辑 :"先查缓存,再查库,最后回填"
-
应用先去 Cache 找。
-
命中(Hit):直接返回数据。
-
未命中(Miss):去数据库(DB)读数据。
-
回填:把从 DB 读到的数据写入 Cache,方便下次用。
-
返回数据。
-
-
写入/添加(Create)逻辑 :"写库"
-
直接写入数据库(主库)。
-
注:新数据通常不需要立即写入缓存,等有人读的时候再利用"读取逻辑"加载(即延迟加载)。
-
-
更改/删除(Update/Delete)逻辑 :"双更难题"
-
这里有一个著名的坑:是"更新缓存"还是"删除缓存"?
-
软考标准答案(也是业界推荐) :先更新数据库,然后删除(失效)缓存。
-
为什么不更新缓存? 因为并发情况下容易产生脏数据。
-
为什么要删除? 让下一次读取请求发现缓存空了,触发"读取逻辑"去数据库拉取最新的,这样最稳。
-
1.3 总结:答题"公式"
1. Cache-Aside Pattern数据不一致解决方案
初始状态: 缓存失效(或为空),数据库中的值为 Old_Value。
线程 2(读) 接收读请求,发现缓存未命中,于是去查询数据库,读取到了 Old_Value。
- 注意:此时线程 2 还没有来得及把数据写入缓存,发生了网络延迟或线程切换。
线程 1(写) 接收写请求,将数据库中的值更新为 New_Value。
线程 1(写) 按照策略删除(淘汰)缓存中的 Key。
线程 2(读) 恢复执行,将第 2 步读取到的 Old_Value 写入缓存。
最终结果: 数据库中是 New_Value(新值),但缓存中却是 Old_Value(旧值)。后续的读请求都会读到旧数据,直到缓存过期。
出现数据不一致。下面有3中解决方案:
延时双删策略:在更新数据库并删除缓存后,延迟一段时间再次删除缓存,以清除可能被读线程回填的脏数据。
设置缓存过期时间:为缓存设置较短的 TTL(生存时间),保证在数据不一致时,旧数据能通过过期自动修正,实现最终一致性。
使用分布式锁:在读写操作时对同一资源加锁,将并发操作串行化,保证强一致性。
2. 遇到"缓存读写策略":
请默写以下伪代码逻辑作为答案骨架:
-
读数据:
javaif (Cache中有数据) { return Cache数据; } else { Data = 读数据库; 写入Cache(Data); return Data; } -
改/删数据:
expand_less
java1. 操作数据库(Update/Delete); 2. 标记Cache失效(Delete Key); // 这一点最重要!
3. 遇到"高可用/无单点故障":
- 答案必须包含关键词:热备份 、主从复制 、集群 、冗余。
2. 数据库设计冲突
2.1. 命名冲突
指相同意义的属性,在不同的分E-R图上有着不同的命名,或是名称相同的属性在不同的分E-R图中代表着不同的意义,这些也要进行统一。
2.2. 属性冲突
指同一属性可能会存在于不同的分E-R中,由于设计人员不同或是出发点不同,对属性的类型、取值范围和数据单位等可能会不一致,这些属性对旧的数据将来只能以一种形式在计算机中存储,这就需要在设计阶段进行统一
2.3. 结构冲突
指同一实体在不同的分E-R图中有不同的属性,同一对象在某一分E-R图中被抽象为实体,而在另一分E-R图中又被抽象为属性
3. 函数依赖和无损连接
设关系模式R(U,F),其中R上的属性集U={A, B, C, D, E},R上的函数依赖集 F={A→B,DE→B,CB→E,E→A, B→D}。++++(1)++++ 为关系R的候选关键字。分解++++(2)++++是无损连接,并保持函数依赖的。
(7)A.AB B.DE C.CE D.DB
(8)A.p = { R1(AC), R2 (ED), R3 (B)} B.p={R1 (AC), R2 (E), R3 (DB) }
C.p={R1(AC), R2 (ED), R3 (AB)} D.p = { R1 (ABC), R2 (ED), R3 (ACE) }
3.1 函数依赖
问题 (1):寻找关系 R 的候选关键字
L-R-N 属性分类法:
L 类(只出现在依赖左边):C(出现在 CB→E 左边,从未出现在右边)。
- 推论:候选关键字必须包含属性 C。
R 类(只出现在依赖右边):无。
LR 类(两边都出现):𝐴,𝐵,𝐷,𝐸。
N 类(两边都不出现):无。
3.2 无损连接
无损连接 的核心判断标准是:分解后的各个子关系通过公共属性连接,能够还原出原关系,不会产生"多余"的元组。可以使用 Chase 算法 或 简单的交集键判断。
-
观察选项 A, B, C:
-
A.
{𝑅1(𝐴𝐶),𝑅2(𝐸𝐷),𝑅3(𝐵)}{R1(AC),R2(ED),R3(B)}:𝑅1和𝑅2没有公共属性,连接变成笛卡尔积,有损。 -
B.
{𝑅1(𝐴𝐶),𝑅2(𝐸),𝑅3(𝐷𝐵)}{R1(AC),R2(E),R3(DB)}:𝑅1和𝑅3没有公共属性,有损。 -
C.
{𝑅1(𝐴𝐶),𝑅2(𝐸𝐷),𝑅3(𝐴𝐵)}{R1(AC),R2(ED),R3(AB)}:𝑅1和𝑅2没有公共属性,𝑅2和𝑅3也没有公共属性。这种分解是断裂的,有损。
-
-
分析选项 D.
{𝑅1(𝐴𝐵𝐶),𝑅2(𝐸𝐷),𝑅3(𝐴𝐶𝐸)}{R1(ABC),R2(ED),R3(ACE)}:连接 R1 和 R3 :公共属性是 AC。在 R1 中有依赖 𝐴→𝐵(虽然 AC不一定是键,但有关联)。连接 R3 和 R2 :公共属性是 E。在 R3 中有依赖E→A。 -
Chase 验证:
第一步:画出初始表(初始状态)
我们需要验证的分解是选项 D:
R1 (A, B, C)
R2 (E, D)
R3 (A, C, E)
我们画一个表格,列是所有属性 𝐴,𝐵,𝐶,𝐷,𝐸。规则 :如果某个子模式里有 这个属性,就填 a(表示有值);如果没有,就填空(或者 b,表示未知)。
(注:下标数字只是为了区分列号,a 代表"真值",b 代表"空值"。我们的目标是把某一行全变成
𝑎)
第二步:开始"填空"(应用函数依赖)
我们要利用题目给出的函数依赖 𝐹={𝐸→𝐴,𝐴→𝐵,𝐵→𝐷,...} 来推导,看能不能把空填上。
1. 利用
𝐸→𝐴
观察 E 列 :发现 R2 和 R3 都有 a5
(也就是它俩在 E 上值相同)。
推导:根据 𝐸→𝐴,既然 E 相同,那么它们的 A 也必须相同。
操作:R3 的 A 是 𝑎1(真值),R2 的 A 是 𝑏21
(空值)。我们把 R2 的 A 填上
𝑎1。
- 利用 𝐴→𝐵,B→D 以此类推最终得到
结论 :R3 这一行变成了 (𝑎1,𝑎2,𝑎3,𝑎4,𝑎5),全是真值,没有任何空缺。
这在数学上证明了:通过这些子关系的连接,我们可以完整地还原出原关系的所有属性,没有信息丢失。 所以,分解是无损连接的。

