Caches are a common component in highly concurrent systems on the Internet, but they add an extra layer that can backfire if not used correctly, such as “Is the cache deleted or updated? “, “Database or cache first?” Today, we’ll talk about a solution for dual-write consistency between caches and databases.

Cache Aside Pattern

In the beginning, the most classic Cache + database read and write Pattern is Cache Aside Pattern.

  • When reading, first read the cache, if the cache is not available, read the database, then take the data and put it into the cache, and return the response.
  • When updating, first update the database and then delete the cache.

Why delete the cache instead of updating it?

Update cache concurrency will bring a variety of problems, directly delete the cache is simpler and safer. Moreover, there is the idea of lazy loading. When you use it, you will read it out and put it in the database. It is not necessary to update it every time, which wastes time and resources.

Update the database before deleting the cache

1. Update database successfully, delete cache successfully, no problem.

2. If the database update fails, the program will catch exceptions, and will not go to the next step, and the data inconsistency will not occur.

3. The database is updated successfully, but the cache fails to be deleted. The database is the new data, the cache is the old data, and an inconsistency has occurred. Here’s how to solve it:

  • The retry mechanism, if the cache deletion fails, we catch the exception and send the key to the message queue.

Then create a consumer purchase yourself and try to delete the key again. (Will cause intrusion to business code)

  • Asynchronously update the cache, when the database is updated, the log is written to the binlog, so we can use a service to listen for changes in the binlog (such as Ali’s canal), and then complete the operation of deleting the key on the client. If the deletion fails, it is sent to the message queue.

In short, in the case of post-delete cache failure, our approach is to keep retrying the delete until successful, to achieve the final consistency!

Delete the cache first, then update the database

1. Delete cache successfully, update database successfully, no problem.

2. If cache deletion fails, the program will catch an exception, and will not go to the next step, and data inconsistency will not occur.

3. Deleting the cache succeeds, but updating the database fails. At this time, the database is old data and the cache is empty, so the data will not be inconsistent.

Although no data inconsistency occurred, it seems to be ok, but in the case of a single thread, the following scenarios may occur if the data is concurrent:

1) Thread A needs to update the data, first remove the Redis cache 2) Thread B queries the data, finds that the cache does not exist, queries the database, writes to Redis, returns 3) Thread A updates the databaseCopy the code

At this time, Redis is the old value, the database is the new value, or the data inconsistency occurred.

Delay double delete

To solve the above situation, we have a delayed double delete strategy. If you do not trust the deletion once, you can delete it again after a certain period of time.

1) delete the cache 2) update the database 3) sleep for 500ms (depending on the time it takes to read the data) 4) delete the cache againCopy the code

The pseudocode is as follows:

public void write(String key,Object data){
   redis.delKey(key);
   db.updateData(data);
   Thread.sleep(500);
   redis.delKey(key);
}
Copy the code

Memory queue

In addition to the delayed double-delete method, there is an alternative to the memory queue. The idea is to serialize, but this way the throughput is too low, affects performance and increases the complexity of the system.

When updating data, instead of directly manipulating the database and cache, we put the Id of the data in an in-memory queue. When we read the data and it’s not in the cache, instead of going to the database and putting it in the cache, we put the Id of the data in the memory queue.

A thread in the background consumes the data in the memory queue and executes it one by one. In this case, an update operation deletes the cache and then updates the database, but the update is not complete. If a read request comes in and an empty cache is read, the cache update request is sent to the queue, which is then backlogged and waits for the cache update to complete.

There is an optimization point. In a queue, it makes no sense to string multiple update cache requests together, so you can filter them. If you find that there is already one update cache request in the queue, you can wait for the previous update request to complete without putting another update request in the queue.

After updating the data in the memory queue is completed, the next operation, the read operation, will read the latest value from the database and then write it to the cache. If the request is still in the waiting time range and the polling finds that the value can be obtained, then the request is returned directly; If the request is waiting longer than a certain amount of time, this time it is read directly from the database.

conclusion

The solutions mentioned above are common and simple, and there is no perfect solution. The final delay of double deletion and memory queue is to solve the problem of deleting the cache first, and then updating the database in concurrent.

The data update of Redis and database discussed today cannot be unified through transactions. We can only take some measures according to corresponding scenarios and costs to reduce the probability of data inconsistency, and achieve a tradeoff between data consistency and performance. Specific scenarios will be used.

Thanks for watching!