面向软件体系结构的关系数据库与面向文档数据库
我在这里经历的是:
- 超级快速复习这两个是什么
- 关键差异
- 优势和劣势
- 系统设计实例(+ Spring Java代码)
- 简史
在这些示例中,我在第一个示例中选择了关系型DB,在另一个示例中选择了面向文档的DB。重点是我为什么做出这样的选择。我还为这两种情况提供了一些示例代码。
在优点和缺点部分,我讨论了过去的优点/缺点以及现在的情况。
两种最常见的DB类型是:
- 关系数据库(RDB):PostgreSQL,MySQL,MSSQL,Oracle DB,.
- 面向文档的数据库(文档存储):MongoDB、DynamoDB、CouchDB.
RDB
关键思想是:将数据放入表中。列是属性,行是值。通过这样做,我们以一种非常结构化的方式获得数据。所以我们有很大的权力来查询数据(使用SQL)。也就是说,我们可以做各种各样的过滤器,关节等,我们安排数据到表中的方式称为数据库模式。
示例表
+----+---------+---------------------+-----+ | ID | Name | Email | Age | +----+---------+---------------------+-----+ | 1 | Alice | alice@example.com | 30 | | 2 | Bob | bob@example.com | 25 | | 3 | Charlie | charlie@example.com | 28 | +----+---------+---------------------+-----+
|
一个数据库可以有多个表。
文档存储
关键思想是:按原样存储数据。假设我们有一个对象。我们只是将其转换为JSON并按原样存储。我们把这些数据称为文档。它不限于JSON,也可以是BSON(二进制JSON)或XML。
示例文档
{ "user_id": 123, "name": "Alice", "email": "alice@example.com", "orders": [ {"id": 1, "item": "Book", "price": 12.99}, {"id": 2, "item": "Pen", "price": 1.50} ] }
每个文档都保存在一个唯一的ID下。此ID可以是路径,例如在Google Cloud Firestore中,但不必是路径。
许多文档“在同一个桶中”被称为集合。我们可以有很多收藏。
差异
模式
- RDB有一个固定的模式。每一行都有相同的模式。
- 文档存储区没有架构。每个文档都可以“有不同的模式”。
数据结构
- RDB将数据分解为规范化的表,表中的关系通过外键实现
- Document将嵌套相关数据直接存储在文档中作为嵌入对象或数组
查询语言
- 关系数据库使用SQL,一种标准化的声明性语言
- 文档存储通常有自己的查询API
的评分办法
- RDB传统上是垂直扩展的(更大/更好的机器)
- 如今,最常见的RDB也提供了水平扩展(例如。PostgeSQL)
- 文档存储非常适合水平扩展(更多机器)
事务支持
ACID =可用性、一致性、隔离性、持久性
- RDB具有成熟的ACID事务支持
- 文档存储传统上牺牲ACID保证来支持性能和可用性
- 现在最常见的文档存储支持ACID(例如,MongoDB)
优缺点
关系数据库
我想在这里再次重复一些已经改变的事情。如前所述,现在大多数文档存储都支持SQL和ACID。同样,现在大多数RDB都支持水平扩展。
让我们以ACID为例。虽然文档存储支持它,但它在RDB中更加成熟。因此,如果你的应用程序在ACID上设置了超高的相关性,那么RDB可能会更好。但如果你的应用只需要基本的ACID,两者都很好用,这不应该是决定性因素。
因此,我把这两点放在括号里,这两点都得到了支持。
优势:
- 数据完整性:强大的模式执行确保数据一致性
- (复杂查询:非常适合跨多个表的复杂连接和聚合)
- (酸)
缺点:
- 模式:虽然模式被列为优势,但它也是一个弱点。更改模式需要进行迁移,这可能会很痛苦
- 对象-关系阻抗不匹配:在应用程序对象和关系表之间进行转换增加了复杂性。Hibernate和其他对象关系映射(ORM)框架可以提供帮助。
- (水平缩放:支持,但与文档存储相比,分片更复杂)
- 初始开发速度:设置模式等需要一些时间
面向文档的数据库
优势:
- 架构灵活性:更适合异构数据结构
- 吞吐量:支持高吞吐量,尤其是写入吞吐量
- (水平缩放:水平缩放更容易,您可以对文档进行分片(计算机A上的文档1-1000,计算机B上的文档1000-2000))
- 基于文档的访问的性能:检索或更新整个文档非常有效
- 一对多关系:在这方面是上级的。不需要连接或其他操作。
- 地点:见下文
- 初始开发速度:由于灵活性,启动速度更快
缺点:
- 复杂的关系:多对一和多对多的关系很困难,通常需要反规范化或应用程序级的连接
- 数据一致性:维护数据完整性的福尔斯责任更多地落在应用程序代码上
- 查询优化:与关系系统相比,
- 存储效率:潜在的数据重复会增加存储需求
- 地点:见下文
局部性
我已经把局部性列为文件存储的优点和缺点。这就是我的意思。
在文档存储中,共时项通常存储为单个连续字符串,以JSON、XML或二进制变体(如MongoDB的BSON)等格式进行编码。当应用程序需要访问整个文档时,这种结构提供了局部性优势。
将相关数据存储在一起可以最大限度地减少磁盘寻道,这与关系数据库(RDB)不同,关系数据库(RDB)中的数据分散在多个表中-这需要多个索引查找,增加了检索时间。
但是,只有当我们需要(几乎)一次查看整个文档时,它才有好处。文档存储通常加载整个文档,即使只有一小部分被访问。这对于大型文档来说是低效的。同样,更新通常需要重写整个文档。因此,为了保持这些缺点小,请确保您的文档很小。
最后一点:局部性并不是文档存储所独有的。例如,Google Spanner或Oracle在关系模型中实现了类似的局部性。
系统设计示例
请注意,我将示例限制到最小,因此文章不会完全臃肿。代码是故意不完整的。您可以在repo的examples文件夹中找到完整的代码。
examples文件夹包含两个完整的应用程序:
- financial-transaction-system -使用关系数据库的Spring靴子和React应用程序(H2)
- content-management-system -使用面向文档的数据库(MongoDB)的Spring靴子和React应用程序
每个示例都有自己的README文件,其中包含运行应用程序的说明。
示例1:金融交易系统
要求
功能要求
- 处理付款和转账
- 保持准确的账户余额
- 存储所有操作的审计跟踪
非功能性需求
为什么这里的关系更好
我们需要可靠性和数据一致性。虽然文档存储也支持这一点(例如ACID),但它们在这方面还不太成熟。我们对文档存储的好处不感兴趣,所以我们使用RDB。
注意事项:如果我们扩展这个示例并添加卖家的配置文件,评级等内容,我们可能需要添加一个单独的数据库,其中我们有不同的优先级,例如可用性和高吞吐量。通过两个独立的数据库,我们可以支持不同的需求并独立扩展它们。
数据模型
Accounts: - account_id (PK = Primary Key) - customer_id (FK = Foreign Key) - account_type - balance - created_at - status
Transactions: - transaction_id (PK) - from_account_id (FK) - to_account_id (FK) - amount - type - status - created_at - reference_number
|
SpringBoot实现
// Entity classes @Entity @Table(name = "accounts") public class Account { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long accountId;
@Column(nullable = false) private Long customerId;
@Column(nullable = false) private String accountType;
@Column(nullable = false) private BigDecimal balance;
@Column(nullable = false) private LocalDateTime createdAt;
@Column(nullable = false) private String status;
// Getters and setters }
@Entity @Table(name = "transactions") public class Transaction { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long transactionId;
@ManyToOne @JoinColumn(name = "from_account_id") private Account fromAccount;
@ManyToOne @JoinColumn(name = "to_account_id") private Account toAccount;
@Column(nullable = false) private BigDecimal amount;
@Column(nullable = false) private String type;
@Column(nullable = false) private String status;
@Column(nullable = false) private LocalDateTime createdAt;
@Column(nullable = false) private String referenceNumber;
// Getters and setters }
// Repository public interface TransactionRepository extends JpaRepository<Transaction, Long> { List<Transaction> findByFromAccountAccountIdOrToAccountAccountId(Long accountId, Long sameAccountId); List<Transaction> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end); }
// Service with transaction support @Service public class TransferService { private final AccountRepository accountRepository; private final TransactionRepository transactionRepository;
@Autowired public TransferService(AccountRepository accountRepository, TransactionRepository transactionRepository) { this.accountRepository = accountRepository; this.transactionRepository = transactionRepository; }
@Transactional public Transaction transferFunds(Long fromAccountId, Long toAccountId, BigDecimal amount) { Account fromAccount = accountRepository.findById(fromAccountId) .orElseThrow(() -> new AccountNotFoundException("Source account not found"));
Account toAccount = accountRepository.findById(toAccountId) .orElseThrow(() -> new AccountNotFoundException("Destination account not found"));
if (fromAccount.getBalance().compareTo(amount) < 0) { throw new InsufficientFundsException("Insufficient funds in source account"); }
// Update balances fromAccount.setBalance(fromAccount.getBalance().subtract(amount)); toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount); accountRepository.save(toAccount);
// Create transaction record Transaction transaction = new Transaction(); transaction.setFromAccount(fromAccount); transaction.setToAccount(toAccount); transaction.setAmount(amount); transaction.setType("TRANSFER"); transaction.setStatus("COMPLETED"); transaction.setCreatedAt(LocalDateTime.now()); transaction.setReferenceNumber(generateReferenceNumber());
return transactionRepository.save(transaction); }
private String generateReferenceNumber() { return "TXN" + System.currentTimeMillis(); } }
|
系统设计示例2:内容管理系统
内容管理系统。
要求
- 存储各种内容类型,包括文章和产品
- 允许添加新内容类型
- 支持评论
非功能性需求
为什么文档存储在这里更好
由于我们没有像上一个示例中那样的关键事务,而是只对性能、可用性和弹性感兴趣,因此文档存储是一个很好的选择。考虑到各种内容类型是一个需求,我们的生活更容易与文档存储,因为他们是模式少。
数据模型
// Article document { "id": "article123", "type": "article", "title": "Understanding NoSQL", "author": { "id": "user456", "name": "Jane Smith", "email": "jane@example.com" }, "content": "Lorem ipsum dolor sit amet...", "tags": ["database", "nosql", "tutorial"], "published": true, "publishedDate": "2025-05-01T10:30:00Z", "comments": [ { "id": "comment789", "userId": "user101", "userName": "Bob Johnson", "text": "Great article!", "timestamp": "2025-05-02T14:20:00Z", "replies": [ { "id": "reply456", "userId": "user456", "userName": "Jane Smith", "text": "Thanks Bob!", "timestamp": "2025-05-02T15:45:00Z" } ] } ], "metadata": { "viewCount": 1250, "likeCount": 42, "featuredImage": "/images/nosql-header.jpg", "estimatedReadTime": 8 } }
// Product document (completely different structure) { "id": "product789", "type": "product", "name": "Premium Ergonomic Chair", "price": 299.99, "categories": ["furniture", "office", "ergonomic"], "variants": [ { "color": "black", "sku": "EC-BLK-001", "inStock": 23 }, { "color": "gray", "sku": "EC-GRY-001", "inStock": 14 } ], "specifications": { "weight": "15kg", "dimensions": "65x70x120cm", "material": "Mesh and aluminum" } }
|
使用MongoDB实现SpringBoot
@Document(collection = "content") public class ContentItem { @Id private String id; private String type; private Map<String, Object> data;
// Common fields can be explicit private boolean published; private Date createdAt; private Date updatedAt;
// The rest can be dynamic @DBRef(lazy = true) private User author;
private List<Comment> comments;
// Basic getters and setters }
// MongoDB Repository public interface ContentRepository extends MongoRepository<ContentItem, String> { List<ContentItem> findByType(String type); List<ContentItem> findByTypeAndPublishedTrue(String type); List<ContentItem> findByData_TagsContaining(String tag); }
// Service for content management @Service public class ContentService { private final ContentRepository contentRepository;
@Autowired public ContentService(ContentRepository contentRepository) { this.contentRepository = contentRepository; }
public ContentItem createContent(String type, Map<String, Object> data, User author) { ContentItem content = new ContentItem(); content.setType(type); content.setData(data); content.setAuthor(author); content.setCreatedAt(new Date()); content.setUpdatedAt(new Date()); content.setPublished(false);
return contentRepository.save(content); }
public ContentItem addComment(String contentId, Comment comment) { ContentItem content = contentRepository.findById(contentId) .orElseThrow(() -> new ContentNotFoundException("Content not found"));
if (content.getComments() == null) { content.setComments(new ArrayList<>()); }
content.getComments().add(comment); content.setUpdatedAt(new Date());
return contentRepository.save(content); }
// Easily add new fields without migrations public ContentItem addMetadata(String contentId, String key, Object value) { ContentItem content = contentRepository.findById(contentId) .orElseThrow(() -> new ContentNotFoundException("Content not found"));
Map<String, Object> data = content.getData(); if (data == null) { data = new HashMap<>(); }
// Just update the field, no schema changes needed data.put(key, value); content.setData(data);
return contentRepository.save(content); } }
|
RDBs vs NoSQL的简史
- 埃德加·科德在1970年发表了一篇论文,提出了RDB
- 关系数据库成为数据库的领导者,主要是由于其可靠性
- NoSQL出现在2009年左右,Facebook和Google等公司开发了定制解决方案来处理其前所未有的规模。他们发表了关于内部数据库系统的论文,激发了MongoDB,Cassandra和Couchbase等开源替代品。
“NoSQL愿望”的主要原因是:
- 需要横向可扩展性
- 更灵活的数据模型
- 性能优化
- 降低运营成本
然而,正如已经提到的,现在RDB也支持这些东西,所以RDB和文档存储之间的明显区别变得越来越模糊。大多数现代数据库都包含了这两种功能。