Welcome to follow our wechat official account: Shishan100

My new course ** “C2C e-commerce System Micro-service Architecture 120-day Practical Training Camp” is online in the public account ruxihu Technology Nest **, interested students, you can click the link below for details:

120-Day Training Camp of C2C E-commerce System Micro-Service Architecture

= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

This article is the public account reader Wei Wu’s submission

Thanks to Wei Wuguexin for sharing technology

= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

Believe that as long as it is a little decent Internet company, more or less have their own a set of cache body

As long as the use of cache, it may involve the cache and database double storage double write, as long as you are double write, there will be data consistency problem, then the author wants to talk with you in this: how to solve the consistency problem?

How to ensure the consistency of cache and database double write is also a very popular question for Java interviewers nowadays!

In general, if you allow cache to be slightly inconsistent with the database occasionally, that is, if your system is not strict about cache + database consistency, it is best not to do this.

That is, read and write requests are serialized and placed in a memory queue to prevent data disorder caused by concurrent requests. The scenario is shown in the figure below:

Note that serialization guarantees that no inconsistencies will occur, but it also causes the throughput of the system to be significantly reduced, requiring several times more machines to support a single request online.

The solution is as follows:

The code implementation is roughly as follows:

/ * * * request service implementation of asynchronous * @ author Administrator / @ * * service (" requestAsyncProcessService ") public class RequestAsyncProcessServiceImpl implements RequestAsyncProcessService { @Override public void process(Request request) { RequestQueue RequestQueue = requestQueue.getInstance (); Map<Integer, Boolean> flagMap = requestQueue.getFlagMap(); If (request instanceof ProductInventoryDBUpdateRequest) {/ / if it is a request to update the database, Flagmap. put(Request.getProductid (), true); } else if(request instanceof ProductInventoryCacheRefreshRequest) { Boolean flag = flagMap.get(request.getProductId()); If (flag == null) {flagmap.put (request.getproductid (), false); flagmap.put (request.getproductid (), false); } // If the flag is not null and is true, there was a previous database update request for the item. = null && flag) { flagMap.put(request.getProductId(), false); } if(flag! = false) {if(flag! = false)} = null && ! Flag) {// For this type of read request, filter it directly, do not put it in the following memory queue return; ArrayBlockingQueue<Request> queue = getRoutingQueue(request.getproductid ()); ArrayBlockingQueue<Request> queue = getRoutingQueue(request.getproductid); Queue.put (request); // Queue. } catch (Exception e) { e.printStackTrace(); @param productId * @return Private ArrayBlockingQueue<Request> getRoutingQueue(Integer productId) { RequestQueue requestQueue = RequestQueue.getInstance(); String key = String.valueof (productId); int h; int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // After modulating the hash value, route the hash value to the specified memory queue size, such as memory queue size 8. Int index = (requestQueue.queuesize () -1) &hash; int index = (requestqueue.queuesize () -1) &hash; System. Out. Println (" = = = = = = = = = = = log = = = = = = = = = = = : routing memory queue, commodity id = "+ productId +", queue index = "+ index); return requestQueue.getQueue(index); }}Copy the code

Cache Aside Pattern

Cache Aside Pattern = Cache Aside Pattern = Cache Aside Pattern

If the cache does not exist, read the database, and then fetch the data and put it in the cache, and return the response. When updating, update the database first and then delete the cache.

Why delete the cache instead of update it?

The reason is simple. A lot of times, in a more complex caching scenario, the cache is not just a value pulled straight out of the database.

For example, one field of a table may be updated, and the corresponding cache may need to query the data of the other two tables and perform operations to calculate the latest value of the cache.

In addition, the cost of updating the cache is sometimes very high. Do you have to update the corresponding cache every time you change the database?

This may be true in some scenarios, but not in more complex scenarios where cached data is computed.

If you frequently modify multiple tables involved in a cache, the cache updates frequently. But the question is, will this cache be accessed frequently?

For example, if a table’s fields are changed 20 times in a minute, or 100 times, the cache is updated 20 times, or 100 times.

But the cache was only read once in a minute, and there was a lot of cold data.

In fact, if you just delete the cache, the cache is recalculated in less than a minute, so the overhead is significantly lower, and the cache is used to calculate the cache.

In fact, deleting the cache, rather than updating it, is the idea of lazy computing. Instead of redoing a complex calculation every time, whether it’s needed or not, let it recalculate until it needs to be used.

For example, Mybatis and Hibernate have lazy loading ideas. When querying a department, the department carries a list of employees. There is no need to check the data of 1000 employees in the department every time.

80% of the time, if you look up the department, you’re just going to access the department, you’re going to look up the department first, and you’re going to look up the employees, and then you’re going to look up 1000 employees in the database only when you’re going to look up the employees.

The most elementary cache inconsistency problem and solution

Problem: Modify database first, then delete cache. If the cache deletion fails, it will result in new data in the database and old data in the cache, causing data inconsistencies.

Delete the cache first and then modify the database. If the database modification fails, the database is old, the cache is empty, and the data is not inconsistent.

The old data in the database is read and then updated to the cache because the cache does not have it at the time of reading.

More complex data inconsistency problem analysis

The data has changed, the cache has been deleted, and the database has been modified.

But before I can modify it, a request comes, reads the cache, finds that the cache is empty, queries the database, finds the old data before modification, and puts it in the cache.

The subsequent data change procedure completes the database modification.

The data in the database is different from the data in the cache…

Why does this problem occur when hundreds of millions of traffic are concurrent?

This problem can only occur when reading or writing data concurrently.

If you have a very low concurrency, especially if you have a very low read concurrency, 10,000 visits per day, then very rarely will you have the kind of inconsistencies that you just described.

However, the problem is that if the daily traffic is hundreds of millions and the concurrent reads per second are tens of thousands, as long as there are data update requests per second, the above database + cache inconsistency may occur.

Solutions are as follows:

When data is updated, operations are routed to an internal JVM queue based on the unique identity of the data.

If the data is not in the cache when it is read, the operation of re-reading the data + updating the cache is routed according to the unique identifier and also sent to the same JVM internal queue.

A queue corresponds to a worker thread, and each worker thread receives the corresponding operation sequentially, and then executes it one by one.

In this case, a data change operation deletes the cache and then updates the database, but the update has not been completed.

If a read request reaches the empty cache, the cache update request can be sent to the queue first. At this point, the cache update request will be backlogged in the queue, and then wait for the cache update to complete synchronously.

There is an optimization point here. In a queue, it doesn’t make sense to string multiple update cache requests together, so you can do filtering

If there is already an update request in the queue, you do not need to put another update request in the queue and wait for the previous update request to complete.

After the worker thread of that queue has finished the database modification of the previous operation, the next operation, the cache update operation, will read the latest value from the database and write it to the cache.

If the request is still in the waiting time range and polling finds that the value can be fetched, it returns directly; If the request waits more than a certain amount of time, the current old value is read directly from the database this time.

In high concurrency scenarios, the following issues should be addressed in this solution:

1. The read request is blocked for a long time

Because read requests are very lightly asynchronous, it is important to be aware of read timeouts, within which each read request must be returned.

In this solution, the biggest risk is that the data may be updated so frequently that a large number of update operations are backlogged in the queue, and then read requests will have a large number of timeouts, resulting in a large number of requests going directly to the database.

So be sure to run some real-world tests to see how often data is updated.

On the other hand, because there may be a backlog of update operations for multiple data items in a queue, you need to test for your own business situation, and you may need to deploy multiple services, each sharing some data update operations.

If 100 inventory modification operations are stored in a memory queue and each inventory modification operation takes 10ms to complete, the data of the last read request may be obtained after 10 * 100 = 1000ms = 1s, which will cause a long time block of the read request.

Therefore, it is important to conduct some stress tests and simulate the online environment according to the actual operation of the business system, to see how many update operations may backlog in the memory queue during the busiest time, and how long the read request corresponding to the last update operation may hang.

If the read request comes back in 200ms, it’s ok if you have a backlog of 10 updates waiting up to 200ms, even during peak hours.

If a memory queue is likely to have a particularly large backlog of updates, then you add machines so that fewer service instances deployed on each machine process less data, and the fewer backlogged updates per memory queue.

In fact, based on the experience of previous projects, data write frequency is generally very low, so in fact, the backlog of updates in the queue should be very small.

For projects like this, with high read concurrency and read cache architecture, write requests are generally very small, and QPS of several hundred per second is good.

As a rough calculation, if you have 500 writes per second, divided into 5 time slices, 100 writes every 200ms, and put them into 20 memory queues, you might have 5 writes per memory queue.

After each write operation performance test, it is generally completed in about 20ms, so for each memory queue data read request, also hang for a while at most, within 200ms can certainly return.

After the simple calculation just now, we know that the write QPS supported by a single machine is no problem in hundreds. If the write QPS is expanded by 10 times, then expand the machine by 10 times, and each machine has 20 queues.

2. The number of concurrent read requests is too high

There is also a risk that a sudden flood of read requests will hang on the service in tens of milliseconds to see if the service can hold up and how many machines are needed to hold up the peak of the maximum limit case.

However, not all data are updated at the same time, and the cache is not invalid at the same time. Therefore, the cache of a few data may be invalid every time, and then the corresponding read requests of those data will come, and the concurrency should not be very large.

3. Request routing for multi-service instance deployment

It is possible that multiple instances of the service are deployed, so it is important to ensure that requests to perform data update operations, as well as cache update operations, are routed through the Nginx server to the same service instance.

For example, read and write requests to the same item are routed to the same machine. You can do your own hash routing between services based on a request parameter, you can also use Nginx’s hash routing function, etc.

4. The routing of hotspot products is faulty, leading to skewed requests

In the event that an item’s read and write requests are so high that they all go to the same queue on the same machine, it may cause too much stress on the same machine.

Because the cache will only be cleared when the commodity data is updated, and then the read and write concurrency will occur, depending on the business system, if the update frequency is not too high, the impact of this problem is not particularly significant, but some machines may have a higher load.

END

“21 days Internet Java Advanced Interview Training Camp (distributed)” details, scan the two-dimensional code at the end of the picture, try the course