再来回顾下之前的思路:
数据更新:根据唯一标识路由到一个队里中,「删除缓存 + 更新数据」
数据读取:如果不在缓存中,根据唯一标识路由到一个队里中,「读取数据 + 写入缓存」
投入队里之后,就等待结果完成,由于同一个标识路由到的是同一个队列中, 所以相当于加锁了。
下面就来实现这个思路,分几步走:
系统初启动时,初始化线程池与内存队列
两种请求对象封装
请求异步执行 service 封装
两种请求 Controller 封装
读请求去重优化
空数据读请求过滤优化
由于该代码核心的就几个地方,其他的都是基础的业务与数据库的常规操作, 故而笔记只记录重点思路地方
系统初启动时,初始化线程池与内存队列
通过 ApplicationRunner 机制,在系统初始化时,对线程池进行初始化操作
/*** 线程与队列初始化**/@Componentpublic class RequestQueue implements ApplicationRunner {private List<ArrayBlockingQueue<Request>> queues = new ArrayList<>();@Overridepublic void run(ApplicationArguments args) throws Exception {int workThread = 10;ExecutorService executorService = Executors.newFixedThreadPool(workThread);for (int i = 0; i < workThread; i++) {ArrayBlockingQueue<Request> queue = new ArrayBlockingQueue<>(100);executorService.submit(new RequestProcessorThread(queue));queues.add(queue);}}public ArrayBlockingQueue<Request> getQueue(int index) {return queues.get(index);}}/*** 处理请求的线程*/public class RequestProcessorThread implements Callable<Boolean> {private ArrayBlockingQueue<Request> queue;public RequestProcessorThread(ArrayBlockingQueue<Request> queue) {this.queue = queue;}@Overridepublic Boolean call() throws Exception {try {while (true) {Request take = queue.take();take.process();}} catch (InterruptedException e) {e.printStackTrace();}return false;}}
两种请求对象封装
数据更新请求
/*** 数据更新请求**/public class ProductInventoryDBUpdateRequest implements Request {private ProductInventory productInventory;private ProductInventoryService productInventoryService;public ProductInventoryDBUpdateRequest(ProductInventory productInventory, ProductInventoryService productInventoryService) {this.productInventory = productInventory;this.productInventoryService = productInventoryService;}@Overridepublic void process() {//1. 删除缓存productInventoryService.removeProductInventoryCache(productInventory.getProductId());//2. 更新库存productInventoryService.updateProductInventory(productInventory);}}
数据刷新请求
/*** 缓存刷新请求*/public class ProductInventoryCacheRefreshRequest implements Request {private Integer productId;private ProductInventoryService productInventoryService;public ProductInventoryCacheRefreshRequest(Integer productId, ProductInventoryService productInventoryService) {this.productId = productId;this.productInventoryService = productInventoryService;}@Overridepublic void process() {// 1. 读取数据库库存ProductInventory productInventory = productInventoryService.findProductInventory(productId);// 2. 设置缓存productInventoryService.setProductInventoryCache(productInventory);}}
请求异步执行 service 封装
@Servicepublic class RequestAsyncProcessServiceImpl implements RequestAsyncProcessService {@Autowiredprivate RequestQueue requestQueue;@Overridepublic void process(Request request) {try {// 1. 根据商品 id 路由到具体的队列ArrayBlockingQueue<Request> queue = getRoutingQueue(request.getProductId());// 2. 放入队列queue.put(request);} catch (InterruptedException e) {e.printStackTrace();}}private ArrayBlockingQueue<Request> getRoutingQueue(Integer productId) {// 先获取 productId 的 hash 值String key = String.valueOf(productId);int h;int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 对hash值取模,将hash值路由到指定的内存队列中,比如内存队列大小8// 用内存队列的数量对hash值取模之后,结果一定是在0~7之间// 所以任何一个商品id都会被固定路由到同样的一个内存队列中去的int index = (requestQueue.queueSize() - 1) & hash;return requestQueue.getQueue(index);}}
两种请求 Controller 封装
/*** 商品库存**/@RestControllerpublic class ProductInventoryController {@Autowiredprivate RequestAsyncProcessService requestAsyncProcessService;@Autowiredprivate ProductInventoryService productInventoryService;/*** 更新商品库存*/@RequestMapping("/updateProductInventory")public Response updateProductInventory(ProductInventory productInventory) {try {ProductInventoryDBUpdateRequest request = new ProductInventoryDBUpdateRequest(productInventory, productInventoryService);requestAsyncProcessService.process(request);return new Response(Response.SUCCESS);} catch (Exception e) {e.printStackTrace();return new Response(Response.FAILURE);}}@RequestMapping("/getProductInventory")public ProductInventory getProductInventory(Integer productId) {try {// 异步获取ProductInventoryCacheRefreshRequest request = new ProductInventoryCacheRefreshRequest(productId, productInventoryService);requestAsyncProcessService.process(request);ProductInventory productInventory = null;long startTime = System.currentTimeMillis();long endTime = 0L;long waitTime = 0L;// 最多等待 200 毫秒while (true) {if (waitTime > 200) {break;}// 尝试去redis中读取一次商品库存的缓存数据productInventory = productInventoryService.getProductInventoryCache(productId);// 如果读取到了结果,那么就返回if (productInventory != null) {return productInventory;}// 如果没有读取到结果,那么等待一段时间else {Thread.sleep(20);endTime = System.currentTimeMillis();waitTime = endTime - startTime;}}// 直接尝试从数据库中读取数据productInventory = productInventoryService.findProductInventory(productId);if (productInventory != null) {return productInventory;}} catch (Exception e) {e.printStackTrace();}return new ProductInventory(productId, -1L);}}
读请求去重优化
核心思路是通过:map 来保存写标志
@Servicepublic class RequestAsyncProcessServiceImpl implements RequestAsyncProcessService {@Autowiredprivate RequestQueue requestQueue;@Overridepublic void process(Request request) {try {Map<Integer, Boolean> flagMap = requestQueue.getFlagMap();// 如果是一个更新数据库请求if (request instanceof ProductInventoryDBUpdateRequest) {flagMap.put(request.getProductId(), true);} else if (request instanceof ProductInventoryCacheRefreshRequest) {Boolean flag = flagMap.get(request.getProductId());// 系统启动后,就没有写请求,全是读,可能导致 flas = nullif (flag == null) {flagMap.put(request.getProductId(), false);}// 已经有过读或写的请求 并且前面已经有一个写请求了if (flag != null && flag) {// 读取请求把,写请求标志冲掉flagMap.put(request.getProductId(), false);}// 如果是读请求,直接返回,等待写完成即可else if (flag != null && !flag) {return;}}// 1. 根据商品 id 路由到具体的队列ArrayBlockingQueue<Request> queue = getRoutingQueue(request.getProductId());// 2. 放入队列queue.put(request);} catch (InterruptedException e) {e.printStackTrace();}}
空数据读请求过滤优化
上面的逻辑,会让这种场景下的请求不执行,但是在 getProductInventory 中,如果从缓存中没有读取到,则最终会走一次数据库。
// 系统启动后,就没有写请求,全是读,可能导致 flas = nullif (flag == null) {flagMap.put(request.getProductId(), false);}
这里就存在一个 bug 了,会导致缓存一直被穿透,如果没有写请求的话,读请求被去重了,一直请求数据库。
修复这个 bug 的话,最简单的办法就是在读取数据库后,直接写入缓存中,如果不考虑并发问题的话,直接在 getProductInventory 中读取数据库后写入缓存即可。
那么就还有一个场景会导致缓存会穿透:数据库中没有这个数据,就会一直走查库的操作,这个问题后续会有解决方案;
深入解决去读请求去重优化
上面的代码存在几个问题:
RequestAsyncProcessServiceImpl.process 判定与设置 flag 值在并发情况下会导致 flag 值问题
查库之后直接写缓存在并发情况下会导致数据不一致的情况(多个请求写数据,队列无意义了)
在 ProductInventoryController 中只要走了数据库后,就强制请求刷新缓存
// 直接尝试从数据库中读取数据productInventory = productInventoryService.findProductInventory(productId);if (productInventory != null) {// 读取到了数据,强制刷新缓存ProductInventoryCacheRefreshRequest forceRfreshRequest = new ProductInventoryCacheRefreshRequest(productId, productInventoryService, true);requestAsyncProcessService.process(forceRfreshRequest);return productInventory;}
每个工作线程,自己处理自己队列的读去重请求
/*** 处理请求的线程** @author : zhuqiang* @date : /4/3 22:38*/public class RequestProcessorThread implements Callable<Boolean> {private ArrayBlockingQueue<Request> queue;/*** k: 商品 id v:请求标志: true : 有更新请求*/private Map<Integer, Boolean> flagMap = new ConcurrentHashMap<>();public RequestProcessorThread(ArrayBlockingQueue<Request> queue) {this.queue = queue;}@Overridepublic Boolean call() throws Exception {try {while (true) {Request request = queue.take();// 非强制刷新请求的话,就是一个正常的读请求if (!request.isForceRfresh()) {// 如果是一个更新数据库请求if (request instanceof ProductInventoryDBUpdateRequest) {flagMap.put(request.getProductId(), true);} else if (request instanceof ProductInventoryCacheRefreshRequest) {Boolean flag = flagMap.get(request.getProductId());if (flag == null) {flagMap.put(request.getProductId(), false);}// 已经有过读或写的请求 并且前面已经有一个写请求了if (flag != null && flag) {// 读取请求把,写请求标志冲掉// 本次读会正常的执行,组成 1+1 (1 写 1 读)// 后续的正常读请求会被过滤掉flagMap.put(request.getProductId(), false);}// 如果是读请求,直接返回,等待写完成即可else if (flag != null && !flag) {continue;}}}request.process();}} catch (InterruptedException e) {e.printStackTrace();}return false;}}
这样一改造之后,并发的地方,就利用队列串行起来了,那么此代码还存在以下场景的缺陷:
当大量请求超过 200 毫秒未获取到缓存,会导致大量请求汇聚到数据库
这种情况的发生场景有:
大量的写请求在前面,导致后续的大量读请求超时,直接读库
数据库压根就没有这个商品,导致缓存被穿透,一直读库
当大量请求超过 200 毫秒后,在数据库获取到了,并请求强制刷新缓存,导致大量请求又回去到数据库了
这种情况是由于增加了强制刷新标志,导致的另外一个 bug,这个时候的思路可以再增加一个强制刷新队列来做强制读请求去重
总结
异步串行化的实现核心思路:
使用队列来避免数据竞争
删除缓存 + 更新数据库 封装成一个写请求
读取数据库 + 写缓存 封装成一个读请求
根据商品 id 路由到同一个队列中(此方案暂未考虑多服务实例的场景)
有写 1+1(1 写 1 读)时,需要过滤掉大量的读请求
这部分正常读请求如不过滤掉,会进入数据库,且库存并未更新,浪费性能资源与缓存穿透(有数据,且数据已经进入了缓存,但是队列中还一直去数据库执行并刷新缓存)
等待读请求需要超时机制,一旦超时则从数据库获取
此类场景出现的时候可能的原因有如下几点:
每个读或写请求测试耗时不准确
测试不准确导致服务实例不够(当然此章节并未解决多服务实例怎么路由或者解决并发的问题)
缓存被穿透,使用不存在的数据一致访问
库存服务代码调试以及打印日志观察服务的运行流程是否正确
创建数据库 product_inventory,两个字段 Integer product_id、Long inventory_cnt
测试场景:
一个写请求:
写请求模拟耗时操作:休眠 10 秒
在写休眠中,来一个读请求
观察日志,是否按照预想流程和结果进行;
在这之前,需要再关键位置添加日志打印,笔记就不贴代码了;
在数据库插入一条数据
INSERT INTO `eshop`.`product_inventory` (`product_id`, `inventory_cnt`) VALUES ('1', '100');
redis 中无数据情况下
在以上场景的基础下,先来模拟 redis 中无数据的情况下的流程是否正确,因为刚好往数据库中增加了数据,还没有往 redis 中增加数据。 正好测试这个场景
// 写请求http://localhost:6001/updateProductInventory?productId=1&inventoryCnt=99// 读请求http://localhost:6001/getProductInventory?productId=1
日志信息
-04-06 20:55:16.257 INFO 9420 --- [nio-6001-exec-1] c.m.c.e.i.w.ProductInventoryController : 更新商品库存请求:商品id=1,库存=99-04-06 20:55:16.258 INFO 9420 --- [nio-6001-exec-1] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 20:55:16.375 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":false,"productId":1}-04-06 20:55:16.376 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 写请求:{"forceRfresh":false,"productId":1}-04-06 20:55:16.440 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 已删除缓存:商品 ID=1-04-06 20:55:16.440 INFO 9420 --- [pool-1-thread-2] .c.e.i.r.ProductInventoryDBUpdateRequest : 写请求:模拟写耗时操作,休眠 10 秒钟// 上面开始模拟耗时操作了-04-06 20:55:17.970 INFO 9420 --- [nio-6001-exec-2] c.m.c.e.i.w.ProductInventoryController : 读取商品库存请求:商品id=1-04-06 20:55:17.971 INFO 9420 --- [nio-6001-exec-2] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 20:55:18.190 INFO 9420 --- [nio-6001-exec-2] c.m.c.e.i.w.ProductInventoryController : 超时 200 毫秒退出尝试,商品 ID=1-04-06 20:55:18.190 INFO 9420 --- [nio-6001-exec-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 数据库获取商品,商品 ID=1// 等待超时,并从数据库获取,获取到了 100 的库存-04-06 20:55:18.234 INFO 9420 --- [nio-6001-exec-2] c.m.c.e.i.w.ProductInventoryController : 缓存未命中,在数据库中查找,商品 ID=1,结果={"inventoryCnt":100,"productId":1}-04-06 20:55:18.234 INFO 9420 --- [nio-6001-exec-2] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 20:55:26.497 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 已更新数据库:商品 ID=1,库存=99// 写完成之后,开始读请求的处理-04-06 20:55:26.499 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":false,"productId":1}-04-06 20:55:26.499 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 读请求:{"forceRfresh":false,"productId":1}-04-06 20:55:26.499 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 1+1 达成,1 写 1 读:{"forceRfresh":false,"productId":1}-04-06 20:55:26.499 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 数据库获取商品,商品 ID=1-04-06 20:55:26.503 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 设置缓存:{"inventoryCnt":99,"productId":1}// 下面的是强制刷新缓存,由于前面耗时操作,导致直接读库并强制刷新缓存操作-04-06 20:55:26.515 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":true,"productId":1}-04-06 20:55:26.515 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 数据库获取商品,商品 ID=1-04-06 20:55:26.523 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 设置缓存:{"inventoryCnt":99,"productId":1}
基于缓存中有库存测试
与上面例子一样,只不过通过了一次测试,现在缓存中有数据了,再继续执行相同的测试操作,观察日志, 库存由 99 变成 98
// 写请求http://localhost:6001/updateProductInventory?productId=1&inventoryCnt=98// 读请求http://localhost:6001/getProductInventory?productId=1
日志输出
-04-06 21:03:00.913 INFO 9420 --- [nio-6001-exec-6] c.m.c.e.i.w.ProductInventoryController : 更新商品库存请求:商品id=1,库存=98-04-06 21:03:00.913 INFO 9420 --- [nio-6001-exec-6] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 21:03:00.913 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":false,"productId":1}-04-06 21:03:00.913 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 写请求:{"forceRfresh":false,"productId":1}-04-06 21:03:00.924 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 已删除缓存:商品 ID=1-04-06 21:03:00.924 INFO 9420 --- [pool-1-thread-2] .c.e.i.r.ProductInventoryDBUpdateRequest : 写请求:模拟写耗时操作,休眠 10 秒钟-04-06 21:03:02.925 INFO 9420 --- [nio-6001-exec-7] c.m.c.e.i.w.ProductInventoryController : 读取商品库存请求:商品id=1-04-06 21:03:02.925 INFO 9420 --- [nio-6001-exec-7] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 21:03:03.147 INFO 9420 --- [nio-6001-exec-7] c.m.c.e.i.w.ProductInventoryController : 超时 200 毫秒退出尝试,商品 ID=1-04-06 21:03:03.147 INFO 9420 --- [nio-6001-exec-7] .m.c.e.i.s.i.ProductInventoryServiceImpl : 数据库获取商品,商品 ID=1-04-06 21:03:03.159 INFO 9420 --- [nio-6001-exec-7] c.m.c.e.i.w.ProductInventoryController : 缓存未命中,在数据库中查找,商品 ID=1,结果={"inventoryCnt":99,"productId":1}-04-06 21:03:03.159 INFO 9420 --- [nio-6001-exec-7] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 21:03:10.937 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 已更新数据库:商品 ID=1,库存=98-04-06 21:03:10.938 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":false,"productId":1}-04-06 21:03:10.938 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 读请求:{"forceRfresh":false,"productId":1}-04-06 21:03:10.938 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 1+1 达成,1 写 1 读:{"forceRfresh":false,"productId":1}-04-06 21:03:10.938 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 数据库获取商品,商品 ID=1-04-06 21:03:10.945 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 设置缓存:{"inventoryCnt":98,"productId":1}-04-06 21:03:10.957 INFO 9420 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":true,"productId":1}-04-06 21:03:10.957 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 数据库获取商品,商品 ID=1-04-06 21:03:10.962 INFO 9420 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 设置缓存:{"inventoryCnt":98,"productId":1}
对于在 200 毫秒内无法返回的数据,无论 redis 中是否存在初始库存数据,流程都一样
基于 200 毫秒内返回写操作完成测试
修改休眠时间,让写操作在 200 毫秒内能正常完成
// 写请求http://localhost:6001/updateProductInventory?productId=1&inventoryCnt=97// 读请求http://localhost:6001/getProductInventory?productId=1
日志输出
-04-06 21:07:42.997 INFO 20676 --- [nio-6001-exec-1] c.m.c.e.i.w.ProductInventoryController : 更新商品库存请求:商品id=1,库存=97-04-06 21:07:42.998 INFO 20676 --- [nio-6001-exec-1] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 21:07:43.209 INFO 20676 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":false,"productId":1}-04-06 21:07:43.209 INFO 20676 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 写请求:{"forceRfresh":false,"productId":1}-04-06 21:07:43.242 INFO 20676 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 已删除缓存:商品 ID=1-04-06 21:07:43.242 INFO 20676 --- [pool-1-thread-2] .c.e.i.r.ProductInventoryDBUpdateRequest : 写请求:模拟写耗时操作,休眠 100 毫秒-04-06 21:07:43.302 INFO 20676 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 已更新数据库:商品 ID=1,库存=97-04-06 21:07:43.622 INFO 20676 --- [nio-6001-exec-2] c.m.c.e.i.w.ProductInventoryController : 读取商品库存请求:商品id=1-04-06 21:07:43.624 INFO 20676 --- [nio-6001-exec-2] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 21:07:43.629 INFO 20676 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":false,"productId":1}-04-06 21:07:43.630 INFO 20676 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 读请求:{"forceRfresh":false,"productId":1}-04-06 21:07:43.630 INFO 20676 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 1+1 达成,1 写 1 读:{"forceRfresh":false,"productId":1}-04-06 21:07:43.630 INFO 20676 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 数据库获取商品,商品 ID=1-04-06 21:07:43.658 INFO 20676 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 设置缓存:{"inventoryCnt":97,"productId":1}-04-06 21:07:43.687 INFO 20676 --- [nio-6001-exec-2] c.m.c.e.i.w.ProductInventoryController : 在缓存中找到,商品 ID=1
由于上面是休眠 100 毫秒,200 毫秒超时,所以人工请求没法模拟出来。
下面的日志输出是休眠 5 秒,10 秒超时
-04-06 21:12:22.725 INFO 21740 --- [nio-6001-exec-1] c.m.c.e.i.w.ProductInventoryController : 更新商品库存请求:商品id=1,库存=97-04-06 21:12:22.726 INFO 21740 --- [nio-6001-exec-1] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 21:12:22.830 INFO 21740 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":false,"productId":1}-04-06 21:12:22.831 INFO 21740 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 写请求:{"forceRfresh":false,"productId":1}-04-06 21:12:23.006 INFO 21740 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 已删除缓存:商品 ID=1-04-06 21:12:23.006 INFO 21740 --- [pool-1-thread-2] .c.e.i.r.ProductInventoryDBUpdateRequest : 写请求:模拟写耗时操作,休眠 5 秒钟-04-06 21:12:23.939 INFO 21740 --- [nio-6001-exec-2] c.m.c.e.i.w.ProductInventoryController : 读取商品库存请求:商品id=1-04-06 21:12:23.940 INFO 21740 --- [nio-6001-exec-2] c.e.i.s.i.RequestAsyncProcessServiceImpl : 路由信息:key=1,商品 ID =1,队列 index=1-04-06 21:12:28.047 INFO 21740 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 已更新数据库:商品 ID=1,库存=97-04-06 21:12:28.050 INFO 21740 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 处理请求:{"forceRfresh":false,"productId":1}-04-06 21:12:28.050 INFO 21740 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 读请求:{"forceRfresh":false,"productId":1}-04-06 21:12:28.050 INFO 21740 --- [pool-1-thread-2] c.m.c.e.i.r.RequestProcessorThread : 1+1 达成,1 写 1 读:{"forceRfresh":false,"productId":1}-04-06 21:12:28.050 INFO 21740 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 数据库获取商品,商品 ID=1-04-06 21:12:28.070 INFO 21740 --- [pool-1-thread-2] .m.c.e.i.s.i.ProductInventoryServiceImpl : 设置缓存:{"inventoryCnt":97,"productId":1}-04-06 21:12:28.086 INFO 21740 --- [nio-6001-exec-2] c.m.c.e.i.w.ProductInventoryController : 在缓存中找到,商品 ID=1
可以看到,在等待超时中,会不断获取缓存中的信息,找到则返回。
此时查看数据库和缓存中的数据都是一致的。