Three hundred rounds of the battle between Redis and database dual-write consistency

Three hundred rounds of the battle between Redis and database dual-write consistency

------------------------------------------- Start ------ -------------------------------------------

The process of ensuring double-write consistency is divided into two steps : read cache and update . There is basically no problem in the read cache step, but there are big differences in the update step.

Cache read:

​ 1. Read the contents of the cache

​ 2. The cache is not read, and the database record is read

​ 3. The records read to the database are returned, and the data records are updated to the redis cache at the same time

There are multiple options for the data update strategy: update the database-update the cache, update the database-delete the cache, delete the cache-update the database

I personally feel that the more controversial issue is the order of updating the database and deleting the cache .

Solution 1: Update the database—>Update the cache redis (the last choice)

Reason 1:

​ A request to update the database (5) -> B request to update the database -> B to update the cache (10) -> A to update the cache (5)

​ From the perspective of thread safety, it can be shot (what about my gun?), dirty data will appear when multiple requests are made for operation updates;

​ Originally in order, the value in the final cache should be updated to 10, but because of the actual network and other objective factors, B first updates the cache to generate dirty data

Reason 2:

​ In the business scenario, if there are more operations to write to the database, the number of cache updates will increase, and the data will be updated before it is read. At the same time, frequent updates also bring performance consumption.

Solution 2: Update the database—>Delete the cache

​ Insufficiency: Concurrency problems occur, but the probability is small;

The reason : appear in the query and update the concurrent request, ps: two update request was still thinking about how to do, feel Zhendi superfluous, completely delete data update will perform caching, and if there is no query did not distinguish; so It is still the case of A-update request and B-query request.

​ The cache is invalid-->①B requests data and gets the old value from the database—>②A updates and writes to the database—>③A deletes the cache—>④B writes the old data to the cache

​ In this case, several conditions need to be met: ①The cache just fails ②The time for writing to the database is shorter than reading the database, so that ③④can be executed in order, but in general, the reading speed is much faster than the writing speed Dirty data can only occur when all the conditions are met. This possibility is difficult to appear (not to say that there is no, don’t arrogate me, I am afraid [the external link image transfer fails, the source site may have an anti-theft link mechanism, it is recommended to save the image directly Upload (img-mNi2yFvI-1622509805031)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\SGPicFaceTpBq\6948\193F65E2.png)])

​ Solution: This kind of concurrency can adopt the strategy of asynchronously deleting the cache to ensure that the dirty cache is cleared after the read request is completed; in addition, the cache deletion failure should also be considered, and the deletion failure will cause the database to be deleted. Update but the cache is still dirty data, I have not encountered this myself, but there are two solutions on the Internet, 1. Use the message queue, put the key that failed to be deleted into the queue for repeated consumption until it succeeds, 2. Use the binlog of the database Log, monitor the binlog program, extract the data key and other information of the operation, use a separate business code to delete, or use the message queue processing, this intrusion on the business code is reduced a lot

Solution 3: Delete the cache—>Update the database (I used it this time)

​ Insufficient: Dirty data will still occur;

The reason : A write operation B update queries --------

​ A write operation, first delete the cache ------> B read, read the old data from the database, write to the cache ------> A write new data to the database

​ In this way, if no expiration time is set for the cache, the read consistency is dirty data before the write request comes;

​ Solution: adopt the delayed double delete strategy, steps: delete the cache----update the database-----sleep for 1 second, delete the cache (delay), in which the operation time of the second deletion of the cache needs to be changed according to the actual situation The main purpose is to clear the old data written into the cache by concurrent read requests during the update process;

The method used for the delayed second deletion is determined according to my own situation. Here I have three options:

​ 1. Thread.sleep(300) forces the currently executing thread to sleep (suspend execution) to "slow down the thread", synchronize the elimination strategy, reduce throughput, and basically use it without impact (the original method used by me)

​ 2. Asynchronous way, use the thread pool to perform the second cache deletion asynchronously (the final use, because it can already be satisfied by itself, the appropriate is the best)

taskExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(400);
                        redisTemplate.delete("CIDLIST::USER_" + resultUser.getId());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

​ 3. Timing tasks, Quartz timing tasks perform secondary cache deletion (as long as it is a timing task tool)

Digression

I also found a way to ensure security. In fact, it is enough for my own business. After all, the most extreme situation is completely fine. Go to the code. The main core idea is to use the key as the unique identifier, and the value as the map set of different locks. The key can be the unique identifier of the user to distinguish. When used by different users, the unique identifiers are inconsistent. The locks used are not the same, and they are executed separately. When a concurrency situation occurs, the key is the same, make a judgment, return to the original old lock and enter the blocking waiting to obtain the lock (personally feel that the essence is still single-threaded, but the efficiency should be higher, because the use of different identifiers to create different Lock to lock different processes)

private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//线程安全的

//加锁    
private void doLock(String lockCode) {
        //不同的值,参数多样化,lockCode,加一个锁----不是同一个key,不能用同一个锁
        ReentrantLock newLock = new ReentrantLock();
        //若已存在,则newLock直接丢弃
        Lock oldLock = locks.putIfAbsent(lockCode, newLock);
        if (oldLock == null) {
            //首次加锁,成功取锁,执行
            newLock.lock();
        } else {
            //阻塞式等待取锁
            oldLock.lock();
        }
    }
//解锁
    private void releaseLock(String userCode) {
        ReentrantLock oldLock = (ReentrantLock) locks.get(userCode);
        //查询锁是否存在和查询当前线程是否保持此锁
        if (oldLock != null && oldLock.isHeldByCurrentThread()) {
            oldLock.unlock();
        }
    }

------------------------------------------- The End ----- --------------------------------------------

---------------- The End -------------------------------- -----------------

It's really gone...Fast one-click three consecutive! ! !