键值存储中的键如何被锁定?

huangapple go评论75阅读模式
英文:

How are keys locked in KV Stores?

问题

我正在构建一个分布式键值存储,只是为了更多地了解分布式系统和并发性。我正在构建的键值存储的实现是完全事务性的,具有内存中的事务日志。为了简单起见,存储也完全在内存中。API公开了GETINSERTUPDATEREMOVE。请注意,所有端点都操作单个键,而不是一系列键。

我通过锁来管理并发。然而,我有一个全局锁,锁定整个数据存储。这听起来非常低效,因为如果我想在更新K2的同时读取K1的值,我必须等待K2完成更新,尽管它们是无关的。

我知道有一些数据库使用更细粒度的锁定。例如,在MySQL服务器中有行级锁。如何实现键级锁定?

我有以下代码:

type Storage struct {
  store map[string]int32
}

我应该添加类似这样的代码吗?:

type Storage struct {
  store map[string]int32
  locks map[string]mutex.Lock
}

如果我这样做,问题在于locks必须与store保持同步。另一个选择是将这两个映射结合起来,但即使这样,如果在GET请求之前有一个REMOVE请求,我仍然会遇到在保持锁的情况下删除映射中的条目的问题。

英文:

I am building a distributed KV store just to learn a little bit more about distributed systems and concurrency. The implementation of the KV store I am building is completely transactional, with an in-memory transaction log. The storage is also completely in-memory just for simplicity. The API exposes the GET, INSERT, UPDATE, REMOVE. Note that all endpoints operate on a single key, not a range of keys.

I am managing concurrency via locks. However, I have a single global lock that locks the entire datastore. This sound terribly inefficient because if I want to read the value for K1 while I update K2, I must wait for k2 to finish updating despite being unrelated.

I know that there are DBs that use more granular locking. For example, in MySQL servers there are row-level locks. How can key-level locks be implemented?

I have

type Storage struct {
  store map[string]int32
}

Should I add something like this?:

type Storage struct {
  store map[string]int32
  locks map[string]mutex.Lock
}

The issue if I do this is that locks has to be kept in sync with the store. Another option would be to combine the two maps, but even then I come into the issue of deleting an entry in the map while the lock is being held if a REMOVE request comes before a GET.

答案1

得分: 1

概念部分

事务

首先,强一致性并不需要事务日志。事务日志对于维护ACID属性是有用的。

在数据库中,强一致性并不严格要求使用事务,但在许多情况下,事务可以是确保一致性的有用工具。

强一致性是指确保数据库的所有读取都返回最新的写入的属性。换句话说,强一致性保证所有客户端都能看到相同的数据,并且数据在整个系统中是最新和一致的。

您可以使用共识算法(如Paxos或Raft)来确保强一致性。在存储数据时,可以使用带有版本的数据,并将其用作Paxos中的ID。

锁定键值存储

在键值(KV)存储中,通常使用某种锁定机制(例如互斥锁或读写锁)来锁定键。这允许多个线程或进程同时访问和修改KV存储中的数据,同时确保数据保持一致和正确。

例如,当线程或进程想要读取或修改KV存储中的特定键时,它可以获取该键的锁。这可以防止其他线程或进程同时修改相同的键,从而导致竞争条件和其他问题。一旦线程或进程完成了对键的读取或修改,它可以释放锁,允许其他线程或进程访问该键。

在KV存储中锁定键的具体细节可以根据KV存储的实现而有所不同。一些KV存储可能使用全局锁(就像您已经在做的那样,有时效率低下)来锁定整个数据存储,而其他KV存储可能使用更细粒度的锁定机制,例如行级或键级锁定,以允许更多并发访问数据。

因此,简而言之,从概念上讲,您是正确的。关键在于锁定的实现细节。

编码

为了严格回答关于锁定的问题,可以考虑使用读写锁(如@paulsm4建议的)。在Go语言中,类似的锁是RWMutex。它在sync.Map中使用。

这是一个简短的示例:

type Storage struct {
  store sync.Map // 并发映射
}

// GET 检索给定键的值。
func (s *Storage) GET(key string) (int32, error) {
  // 获取键的读锁。
  v, ok := s.store.Load(key)
  if !ok {
    return 0, fmt.Errorf("未找到键:%s", key)
  }

  // 返回值。
  return v.(int32), nil
}

// INSERT 将给定的键值对插入到数据存储中。
func (s *Storage) INSERT(key string, value int32) error {
  // 获取键的写锁。
  s.store.Store(key, value)
  return nil
}

// UPDATE 更新给定键的值。
func (s *Storage) UPDATE(key string, value int32) error {
  // 获取键的写锁。
  s.store.Store(key, value)
  return nil
}

// REMOVE 从数据存储中删除给定键的键值对。
func (s *Storage) REMOVE(key string) error {
  // 获取键的写锁。
  s.store.Delete(key)
  return nil
}

您需要在此基础上使用Paxos来确保副本之间的一致性。

英文:

Concetual part

Transactions

First, transaction logs are not needed for strong consitency. Transaction logs are useful for upholding ACID properties.

Transactions are also not strictly required for strong consistency in a database, but they can be a useful tool for ensuring consistency in many situations.

Strong consistency refers to the property that ensures that all reads of a database will return the most recent write, regardless of where the read operation is performed. In other words, strong consistency guarantees that all clients will see the same data, and that the data will be up-to-date and consistent across the entire system.

You can use a consensus algorithm, such as Paxos or Raft, to assure strong consistency. When storing data, you can store data with a version, and use that as the ID in Paxos.

Locking in KV Stores

In a key-value (KV) store, keys are typically locked using some kind of locking mechanism, such as a mutex or a reader-writer lock (as suggested by @paulsm4). This allows multiple threads or processes to access and modify the data in the KV store concurrently, while still ensuring that the data remains consistent and correct.

For example, when a thread or process wants to read or modify a particular key in the KV store, it can acquire a lock for that key. This prevents other threads or processes from concurrently modifying the same key, which can lead to race conditions and other problems. Once the thread or process has finished reading or modifying the key, it can release the lock, allowing other threads or processes to access the key.

The specific details of how keys are locked in a KV store can vary depending on the implementation of the KV store. Some KV stores may use a global lock (as you were already doing, which is sometimes inefficient) that locks the entire data store, while others may use more granular locking mechanisms, such as row-level or key-level locks, to allow more concurrent access to the data.

So tldr; conceptually, you were right. The devil is in the details of the implementation of the locking.

Coding

To strictly answer the question about locking, one can consider Readers-writers locks as suggested by @paulsm4. In golang, a similar lock is RWMutex. It is used in sync.Map.

Here is a short example:

type Storage struct {
  store sync.Map // a concurrent map
}

// GET retrieves the value for the given key.
func (s *Storage) GET(key string) (int32, error) {
  // Acquire a read lock for the key.
  v, ok := s.store.Load(key)
  if !ok {
    return 0, fmt.Errorf("key not found: %s", key)
  }

  // Return the value.
  return v.(int32), nil
}

// INSERT inserts the given key-value pair into the data store.
func (s *Storage) INSERT(key string, value int32) error {
  // Acquire a write lock for the key.
  s.store.Store(key, value)
  return nil
}

// UPDATE updates the value for the given key.
func (s *Storage) UPDATE(key string, value int32) error {
  // Acquire a write lock for the key.
  s.store.Store(key, value)
  return nil
}

// REMOVE removes the key-value pair for the given key from the data store.
func (s *Storage) REMOVE(key string) error {
  // Acquire a write lock for the key.
  s.store.Delete(key)
  return nil
}

You would need Paxos on top of this to ensure consistency accross replicas.

huangapple
  • 本文由 发表于 2022年12月11日 02:41:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/74755667.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定