01

Extending Log-Based Storage to a Simple Key-Value Database

The log-based storage design from our messaging system can be extended to support a simple key-value database.

02

The Four Core CRUD Operations

  • Create: Append a new record to the log file.
  • Read: Use an in-memory index to locate the record, then seek to that position in the file.
  • Update: Append a new record with the same key to the end of the log segment. Update the index to point to the latest record.
  • Delete: Append a tombstone record to mark a key as deleted.
03

How Update Works

An update operation appends a new record with the existing key to the end of the log segment. The index is updated to reference the latest record position. This results in duplicate keys in the log file, but the index always points to the most recent version.

04

The Delete Operation

Deleting a record is more complex than the other operations. In messaging systems, we delete entire segments after all messages are consumed. However, this assumption does not hold for general databases.

In typical databases, some records are deleted while others remain. Therefore, we cannot rely on deleting entire segments to free disk space.

05

Tombstone Records

To address deletion, we append a tombstone record to the data file when a key is deleted. This record signals that the value is no longer valid.

When a query is made for a deleted key, the system checks for a tombstone record. If one exists, no value is returned.

06

Potential Concerns

  • Disk Space Exhaustion: Records are continuously appended, leading to unbounded growth.
  • Index Scalability: With many keys, in-memory indexing becomes insufficient. Disk-based indexes introduce random access overhead.
  • Range Query Inefficiency: The design supports point queries only. Range queries—like finding all employees with more than five years of service—require scanning every record.
07

Log Compaction as a Solution

To mitigate unbounded growth, we implement log compaction. This process retains only the most recent record for each key, removing all previous versions.

Log compaction ensures that only the latest value for each key is preserved, while all older records are deleted.

This approach prevents disk space exhaustion by eliminating redundant records over time.

The design remains effective for point queries but is limited in handling range queries and large-scale indexing.

These limitations point to the need for a more advanced design in future videos.

08

Background Thread and Segment Rotation

A background thread runs periodically to manage segment files. It creates a new segment file and copies records from the old segment to the new one.

09

Duplicate Key Removal During Copy

While copying records, the process identifies and removes duplicate keys. This ensures each key appears only once in the new segment.

10

Log Compaction and Deleted Records

During log compaction, deleted records are eliminated. All previous versions of a deleted key are discarded entirely.

11

Segment Replacement

Once all records from the old segment are copied to the new file, the old segment is replaced by the new one. The old segment is then deleted from disk.

12

Segment Size Reduction

Compaction reduces segment size by removing duplicates and deleted records. Smaller segments result from this process.

13

Segment Merging

Smaller segments can be merged together to reduce the total number of segments in storage.

14

Index Efficiency Improvement

Fewer segments mean fewer hash table indexes. During key lookups, fewer indexes need to be checked, improving performance.

15

BitCask Storage Engine

BitCask, the default storage engine in React, implements these principles. It is designed for fast reads and writes.

16

Write Performance

Writes are fast because appending to the end of a file is efficient, and updating the in-memory hash table index is quick.

17

Read Performance

Reads are fast because they require only a single disk seek to retrieve a value.

18

Design Simplicity

Complex problems can be solved with simple, combined ideas. Designing a storage backend for a distributed key-value data store initially seems daunting.

19

Problem Solvability

With foundational concepts applied correctly, such problems become manageable and less intimidating.

Questions

  • How does the database handle updates when the same key is written multiple times?
  • What happens when a record is deleted, and how is that reflected during a read operation?
  • Could the system run out of disk space if records are constantly appended without deletion?
  • What limitation does this design have when querying data in ranges (e.g., all employees with >5 years of experience)?
  • How does log compaction work, and what does it achieve in terms of storage and performance?
  • Why might storing a large number of keys in memory be a concern for this design?
  • Is it possible to retrieve a deleted value after a tombstone has been added?
  • What happens to old segments after log compaction completes?

Key takeaways

  • Create: append a record with key, value, and metadata to the log file.
  • Read: use an in-memory index to locate the record and retrieve it from the log.
  • Update: append a new record with the same key; the index points to the latest record.
  • Delete: append a tombstone record; on read, if a tombstone exists, no value is returned.
  • The system continuously appends records, which raises concerns about disk space and performance.
  • Large numbers of keys may exceed memory capacity, requiring disk-based indexes and increasing random access overhead.
  • Range queries are not supported efficiently because every record must be checked individually.
  • Log compaction periodically removes duplicates and deleted records, reducing segment size and index load.
  • The design is optimized for point queries and fast writes, but not for range queries or large-scale key storage.