商品详情页系统架构-笔记5-结合代码实现

目录

Cache aside pattern

双写不一致,怎么保证

数据库与缓存更新与读取操作进行异步串行化

1、线程池+内存

2、两种请求对象封装

3、请求异步执行Service封装

4、请求处理的工作线程封装

5、两种请求Controller接口封装

6、读请求去重优化

7、空数据的读请求过滤优化

8、其他

受不了了,上面的都太复杂了,有没有简单的总结一下?

 方案1:


Cache aside pattern

最经典的缓存+数据库读写的模式,cache aside pattern

1、Cache Aside Pattern

(1)读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应

(2)更新的时候,先删除缓存,然后再更新数据库


2、为什么是删除缓存,而不是更新缓存呢?

原因很简单,很多时候,复杂点的缓存的场景,因为缓存有的时候,不简单是数据库中直接取出来的值,主要可能是缓存逻辑复杂,代价大。

 

双写不一致,怎么保证


1、最初级的缓存不一致问题以及解决方案

问题:先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致

解决思路

先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致

因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中

2、比较复杂的数据不一致问题分析

数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改完成。

一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中

数据变更的程序完成了数据库的修改

完了,数据库和缓存中的数据不一样了。。。。

3、为什么上亿流量高并发场景下,缓存会出现这个问题?

只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题

其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就1万次,那么很少的情况下,会出现刚才描述的那种不一致的场景

但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况

高并发了以后,问题是很多的

4、数据库与缓存更新与读取操作进行异步串行化(终极方法)

下面的方式,觉得实现异常复杂,没有必要

更新数据的时候,根据数据的唯一标识,将操作路由(取模)之后,发送到一个jvm内部的队列中

读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中

一个队列对应一个工作线程

每个工作线程串行拿到对应的操作,然后一条一条的执行

这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新

此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成

这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可

待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中

如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值

 

数据库与缓存更新与读取操作进行异步串行化

 

队列

对一个商品的库存的数据库更新操作已经在内存队列中了

然后对这个商品的库存的读取操作,要求读取数据库的库存数据,然后更新到缓存中,多个读

这多个读,其实只要有一个读请求操作压到队列里就可以了

其他的读操作,全部都wait那个读请求的操作,刷新缓存,就可以读到缓存中的最新数据了

对应的代码:

1、线程池+内存

InitListener:系统初始化监听器

public class InitListener implements ServletContextListener {

	@Override
	public void contextInitialized(ServletContextEvent sce) {
		// 初始化工作线程池和内存队列
		RequestProcessorThreadPool.init();
	}
	
	@Override
	public void contextDestroyed(ServletContextEvent sce) {
		
	}

}

然后 new InitListener() : 注册监听器

@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean(){
    ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean();
    servletListenerRegistrationBean.setListener(new InitListener());
    return servletListenerRegistrationBean;
}

RequestProcessorThreadPool : 请求处理线程池:单例

/**
 * 请求处理线程池:单例
 * @author Administrator
 *
 */
public class RequestProcessorThreadPool {
	
	// 在实际项目中,你设置线程池大小是多少,每个线程监控的那个内存队列的大小是多少
	// 都可以做到一个外部的配置文件中
	// 我们这了就给简化了,直接写死了,好吧
	
	/**
	 * 线程池
	 */
	private ExecutorService threadPool = Executors.newFixedThreadPool(10);
	
	public RequestProcessorThreadPool() {
		RequestQueue requestQueue = RequestQueue.getInstance();
		
		for(int i = 0; i < 10; i++) {
			ArrayBlockingQueue<Request> queue = new ArrayBlockingQueue<Request>(100);
			requestQueue.addQueue(queue);  
			threadPool.submit(new RequestProcessorThread(queue));  
		}
	}

	/**
	 * 单例有很多种方式去实现:我采取绝对线程安全的一种方式
	 * 
	 * 静态内部类的方式,去初始化单例
	 * 
	 * @author Administrator
	 *
	 */
	private static class Singleton {
		
		private static RequestProcessorThreadPool instance;
		
		static {
			instance = new RequestProcessorThreadPool();
		}
		
		public static RequestProcessorThreadPool getInstance() {
			return instance;
		}
		
	}
	
	/**
	 * jvm的机制去保证多线程并发安全
	 * 
	 * 内部类的初始化,一定只会发生一次,不管多少个线程并发去初始化
	 * 
	 * @return
	 */
	public static RequestProcessorThreadPool getInstance() {
		return Singleton.getInstance();
	}
	
	/**
	 * 初始化的便捷方法
	 */
	public static void init() {
		getInstance();
	}
	
}
RequestProcessorThread : 执行请求的工作线程

线程里面会不断的while(true) 消费请求去执行。

/**
 * 执行请求的工作线程
 * @author Administrator
 *
 */
public class RequestProcessorThread implements Callable<Boolean> {
	
	/**
	 * 自己监控的内存队列
	 */
	private ArrayBlockingQueue<Request> queue;

	public RequestProcessorThread(ArrayBlockingQueue<Request> queue) {
		this.queue = queue;
	}
	
	@Override
	public Boolean call() throws Exception {
		try {
			while(true) {
				// ArrayBlockingQueue
				// Blocking就是说明,如果队列满了,或者是空的,那么都会在执行操作的时候,阻塞住
				Request request = queue.take();
				boolean forceRfresh = request.isForceRefresh();
				
				// 先做读请求的去重
				if(!forceRfresh) {
					RequestQueue requestQueue = RequestQueue.getInstance();
					Map<Integer, Boolean> flagMap = requestQueue.getFlagMap();
					
					if(request instanceof ProductInventoryDBUpdateRequest) {
						// 如果是一个更新数据库的请求,那么就将那个productId对应的标识设置为true
						flagMap.put(request.getProductId(), true);
					} else if(request instanceof ProductInventoryCacheRefreshRequest) {
						Boolean flag = flagMap.get(request.getProductId());
						
						// 如果flag是null
						if(flag == null) {
							flagMap.put(request.getProductId(), false);
						}
						
						// 如果是缓存刷新的请求,那么就判断,如果标识不为空,而且是true,就说明之前有一个这个商品的数据库更新请求
						if(flag != null && flag) {
							flagMap.put(request.getProductId(), false);
						}
						
						// 如果是缓存刷新的请求,而且发现标识不为空,但是标识是false
						// 说明前面已经有一个数据库更新请求+一个缓存刷新请求了,大家想一想
						if(flag != null && !flag) {
							// 对于这种读请求,直接就过滤掉,不要放到后面的内存队列里面去了
							return true;
						}
					}
				}
				
				System.out.println("===========日志===========: 工作线程处理请求,商品id=" + request.getProductId()); 
				// 执行这个request操作
				request.process();
				
				// 假如说,执行完了一个读请求之后,假设数据已经刷新到redis中了
				// 但是后面可能redis中的数据会因为内存满了,被自动清理掉
				// 如果说数据从redis中被自动清理掉了以后
				// 然后后面又来一个读请求,此时如果进来,发现标志位是false,就不会去执行这个刷新的操作了
				// 所以在执行完这个读请求之后,实际上这个标志位是停留在false的
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return true;
	}

}
RequestQueue : 请求内存队列单独提到一个类里面

创建10个内存队列,这些内存队列多扔到RequestQueue里面,同时这些内存队列跟线程绑定在一起。

RequestQueue也做成单例。 

/**
 * 请求内存队列
 * @author Administrator
 *
 */
public class RequestQueue {

	/**
	 * 内存队列
	 */
	private List<ArrayBlockingQueue<Request>> queues = 
			new ArrayList<ArrayBlockingQueue<Request>>();
	/**
	 * 标识位map
	 */
	private Map<Integer, Boolean> flagMap = new ConcurrentHashMap<Integer, Boolean>();
	
	/**
	 * 单例有很多种方式去实现:我采取绝对线程安全的一种方式
	 * 
	 * 静态内部类的方式,去初始化单例
	 * 
	 * @author Administrator
	 *
	 */
	private static class Singleton {
		
		private static RequestQueue instance;
		
		static {
			instance = new RequestQueue();
		}
		
		public static RequestQueue getInstance() {
			return instance;
		}
		
	}
	
	/**
	 * jvm的机制去保证多线程并发安全
	 * 
	 * 内部类的初始化,一定只会发生一次,不管多少个线程并发去初始化
	 * 
	 * @return
	 */
	public static RequestQueue getInstance() {
		return Singleton.getInstance();
	}
	
	/**
	 * 添加一个内存队列
	 * @param queue
	 */
	public void addQueue(ArrayBlockingQueue<Request> queue) {
		this.queues.add(queue);
	}
	
	/**
	 * 获取内存队列的数量
	 * @return
	 */
	public int queueSize() {
		return queues.size();
	}
	
	/**
	 * 获取内存队列
	 * @param index
	 * @return
	 */
	public ArrayBlockingQueue<Request> getQueue(int index) {
		return queues.get(index);
	}
	
	public Map<Integer, Boolean> getFlagMap() {
		return flagMap;
	}
	
}

2、两种请求对象封装

一个请求是发生交易之后更新数据库的请求;另一个是读请求(其实就是把数据库的数据读出来更新到缓存中)。

先准备service【商品库存Service接口】:

/**
 * 商品库存Service接口
 * @author Administrator
 *
 */
public interface ProductInventoryService {

	/**
	 * 更新商品库存
	 * @param productInventory 商品库存
	 */
	void updateProductInventory(ProductInventory productInventory);
	
	/**
	 * 删除Redis中的商品库存的缓存
	 * @param productInventory 商品库存
	 */
	void removeProductInventoryCache(ProductInventory productInventory);
	
	/**
	 * 根据商品id查询商品库存
	 * @param productId 商品id 
	 * @return 商品库存
	 */
	ProductInventory findProductInventory(Integer productId);
	
	/**
	 * 设置商品库存的缓存
	 * @param productInventory 商品库存
	 */
	void setProductInventoryCache(ProductInventory productInventory);
	
	/**
	 * 获取商品库存的缓存
	 * @param productId
	 * @return
	 */
	ProductInventory getProductInventoryCache(Integer productId);
	
}

【一个请求是发生交易之后更新数据库的请求】:

/**
 * 比如说一个商品发生了交易,那么就要修改这个商品对应的库存
 * 
 * 此时就会发送请求过来,要求修改库存,那么这个可能就是所谓的data update request,数据更新请求
 * 
 * cache aside pattern
 * 
 * (1)删除缓存
 * (2)更新数据库
 * 
 * 项目,我们尽可能在电商的业务背景下,用整个电商的业务串起来,讲解,在项目里去学习知识,而不是干讲各种解决方案
 * 
 * 真实的场景,有大量的业务在里面,涉及几十个字段,可能过来的是一个什么什么请求
 * 
 * 然后你得计算之后,才知道它最终的库存是多少
 * 
 * 
 * @author Administrator
 *
 */
public class ProductInventoryDBUpdateRequest implements Request {

	/**
	 * 商品库存
	 */
	private ProductInventory productInventory;
	/**
	 * 商品库存Service
	 */
	private ProductInventoryService productInventoryService;
	
	public ProductInventoryDBUpdateRequest(ProductInventory productInventory,
			ProductInventoryService productInventoryService) {
		this.productInventory = productInventory;
		this.productInventoryService = productInventoryService;
	}
	
	@Override
	public void process() {
		System.out.println("===========日志===========: 数据库更新请求开始执行,商品id=" + productInventory.getProductId() + ", 商品库存数量=" + productInventory.getInventoryCnt());  
		// 删除redis中的缓存
		productInventoryService.removeProductInventoryCache(productInventory);
		// 为了模拟演示先删除了redis中的缓存,然后还没更新数据库的时候,读请求过来了,这里可以人工sleep一下
//		try {
//			Thread.sleep(20000);
//		} catch (InterruptedException e) {
//			e.printStackTrace();
//		} 
		// 修改数据库中的库存
		productInventoryService.updateProductInventory(productInventory);  
	}
	
	/**
	 * 获取商品id
	 */
	public Integer getProductId() {
		return productInventory.getProductId();
	}

	@Override
	public boolean isForceRefresh() {
		return false;
	}
	
}

【另一个是读请求(其实就是把数据库的数据读出来更新到缓存中)】: 

/**
 * 重新加载商品库存的缓存
 * @author Administrator
 *
 */
public class ProductInventoryCacheRefreshRequest implements Request {

	/**
	 * 商品id
	 */
	private Integer productId;
	/**
	 * 商品库存Service
	 */
	private ProductInventoryService productInventoryService;
	/**
	 * 是否强制刷新缓存
	 */
	private boolean forceRefresh;
	
	public ProductInventoryCacheRefreshRequest(Integer productId,
			ProductInventoryService productInventoryService,
			boolean forceRefresh) {
		this.productId = productId;
		this.productInventoryService = productInventoryService;
		this.forceRefresh = forceRefresh;
	}
	
	@Override
	public void process() {
		// 从数据库中查询最新的商品库存数量
		ProductInventory productInventory = productInventoryService.findProductInventory(productId);
		System.out.println("===========日志===========: 已查询到商品最新的库存数量,商品id=" + productId + ", 商品库存数量=" + productInventory.getInventoryCnt());  
		// 将最新的商品库存数量,刷新到redis缓存中去
		productInventoryService.setProductInventoryCache(productInventory); 
	}
	
	public Integer getProductId() {
		return productId;
	}

	public boolean isForceRefresh() {
		return forceRefresh;
	}
	
}

3、请求异步执行Service封装

这一步的作用:主要做请求路由和其他优化的。

做请求的路由,根据每个请求的商品id,路由到对应的内存队列中去;然后将请求放入对应的队列中,完成路由操作。

/**
 * 请求异步处理的service实现
 * @author Administrator
 *
 */
@Service("requestAsyncProcessService")  
public class RequestAsyncProcessServiceImpl implements RequestAsyncProcessService {
	
	@Override
	public void process(Request request) {
		try {
			// 做请求的路由,根据每个请求的商品id,路由到对应的内存队列中去
			ArrayBlockingQueue<Request> queue = getRoutingQueue(request.getProductId());
			// 将请求放入对应的队列中,完成路由操作
			queue.put(request);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 获取路由到的内存队列
	 * @param productId 商品id
	 * @return 内存队列
	 */
	private ArrayBlockingQueue<Request> getRoutingQueue(Integer productId) {
		RequestQueue requestQueue = RequestQueue.getInstance();
		
		// 先获取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;
		
		System.out.println("===========日志===========: 路由内存队列,商品id=" + productId + ", 队列索引=" + index);  
		
		return requestQueue.getQueue(index);
	}

}

 

4、请求处理的工作线程封装

为啥用ArrayBlockingQueue

因为 Blocking就是说明,如果队列满了,或者是空的,那么都会在执行操作的时候,阻塞住

RequestProcessorThread:
@Override
	public Boolean call() throws Exception {
		try {
			while(true) {
				// ArrayBlockingQueue
				// Blocking就是说明,如果队列满了,或者是空的,那么都会在执行操作的时候,阻塞住
				Request request = queue.take();
				// 执行这个request操作
				request.process();          
  	        } catch (Exception e) {
			    e.printStackTrace();
		    }
		    return true;
	}

}

5、两种请求Controller接口封装

ProductInventoryController

【更新商品库存】:

	@Resource
	private RequestAsyncProcessService requestAsyncProcessService;
	@Resource
	private ProductInventoryService productInventoryService;
	
	/**
	 * 更新商品库存
	 */
	@RequestMapping("/updateProductInventory")
	@ResponseBody
	public Response updateProductInventory(ProductInventory productInventory) {
		Response response = null;
		
		try {
			Request request = new ProductInventoryDBUpdateRequest(
					productInventory, productInventoryService);
			requestAsyncProcessService.process(request);
			response = new Response(Response.SUCCESS);
		} catch (Exception e) {
			e.printStackTrace();
			response = new Response(Response.FAILURE);
		}
		
		return response;
	}

【读取商品库存】:

/**
	 * 获取商品库存
	 */
	@RequestMapping("/getProductInventory")
	@ResponseBody
	public ProductInventory getProductInventory(Integer productId) {
		System.out.println("===========日志===========: 接收到一个商品库存的读请求,商品id=" + productId);  
		
		ProductInventory productInventory = null;
		
		try {
			Request request = new ProductInventoryCacheRefreshRequest(
					productId, productInventoryService, false);
			requestAsyncProcessService.process(request);
			
			// 将请求扔给service异步去处理以后,就需要while(true)一会儿,在这里hang住
			// 去尝试等待前面有商品库存更新的操作,同时缓存刷新的操作,将最新的数据刷新到缓存中
			long startTime = System.currentTimeMillis();
			long endTime = 0L;
			long waitTime = 0L;
			
			// 等待超过200ms没有从缓存中获取到结果
			while(true) {
//				if(waitTime > 25000) {
//					break;
//				}
				
				// 一般公司里面,面向用户的读请求控制在200ms就可以了
				if(waitTime > 200) {
					break;
				}
				
				// 尝试去redis中读取一次商品库存的缓存数据
				productInventory = productInventoryService.getProductInventoryCache(productId);
				
				// 如果读取到了结果,那么就返回
				if(productInventory != null) {
					System.out.println("===========日志===========: 在200ms内读取到了redis中的库存缓存,商品id=" + productInventory.getProductId() + ", 商品库存数量=" + productInventory.getInventoryCnt());  
					return productInventory;
				}
				
				// 如果没有读取到结果,那么等待一段时间
				else {
					Thread.sleep(20);
					endTime = System.currentTimeMillis();
					waitTime = endTime - startTime;
				}
			}
			
			// 直接尝试从数据库中读取数据
			productInventory = productInventoryService.findProductInventory(productId);
			if(productInventory != null) {
				// 将缓存刷新一下
				// 这个过程,实际上是一个读操作的过程,但是没有放在队列中串行去处理,还是有数据不一致的问题
				request = new ProductInventoryCacheRefreshRequest(
						productId, productInventoryService, true);
				requestAsyncProcessService.process(request);
				
				// 代码会运行到这里,只有三种情况:
				// 1、就是说,上一次也是读请求,数据刷入了redis,但是redis LRU算法给清理掉了,标志位还是false
				// 所以此时下一个读请求是从缓存中拿不到数据的,再放一个读Request进队列,让数据去刷新一下
				// 2、可能在200ms内,就是读请求在队列中一直积压着,没有等待到它执行(在实际生产环境中,基本是比较坑了)
				// 所以就直接查一次库,然后给队列里塞进去一个刷新缓存的请求
				// 3、数据库里本身就没有,缓存穿透,穿透redis,请求到达mysql库
				
				return productInventory;
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return new ProductInventory(productId, -1L);  
	}



--------------------------------------------------------------------------------------
   ProductInventoryServiceImpl:
   /**
	 * 获取商品库存的缓存
	 * @param productId
	 * @return
	 */
	public ProductInventory getProductInventoryCache(Integer productId) {
		Long inventoryCnt = 0L;
		
		String key = "product:inventory:" + productId;
		String result = redisDAO.get(key);
		
		if(result != null && !"".equals(result)) {
			try {
				inventoryCnt = Long.valueOf(result);
				return new ProductInventory(productId, inventoryCnt);
			} catch (Exception e) {
				e.printStackTrace(); 
			}
		}
		
		return null;
	}

6、读请求去重优化

如果一个读请求过来,发现前面已经有一个写请求和一个读请求了,那么这个读请求就不需要压入队列中了

因为那个写请求肯定会更新数据库,然后那个读请求肯定会从数据库中读取最新数据,然后刷新到缓存中,自己只要hang一会儿就可以从缓存中读到数据了

RequestQueue:
public class RequestQueue {
	/**
	 * 标识位map
	 */
	private Map<Integer, Boolean> flagMap = new ConcurrentHashMap<Integer, Boolean>();
	
    public Map<Integer, Boolean> getFlagMap() {
		return flagMap;
	}
RequestAsyncProcessServiceImpl:
@Service("requestAsyncProcessService")  
public class RequestAsyncProcessServiceImpl implements RequestAsyncProcessService {
	
	@Override
	public void process(Request request) {
		try {
			// 先做读请求的去重
			RequestQueue requestQueue = RequestQueue.getInstance();
			Map<Integer, Boolean> flagMap = requestQueue.getFlagMap();
			
			if(request instanceof ProductInventoryDBUpdateRequest) {
				// 如果是一个更新数据库的请求,那么就将那个productId对应的标识设置为true
				flagMap.put(request.getProductId(), true);
			} else if(request instanceof ProductInventoryCacheRefreshRequest) {
				Boolean flag = flagMap.get(request.getProductId());
				
				// 如果flag是null
				if(flag == null) {
					flagMap.put(request.getProductId(), false);
				}
				
				// 如果是缓存刷新的请求,那么就判断,如果标识不为空,而且是true,就说明之前有一个这个商品的数据库更新请求
				if(flag != null && flag) {
					flagMap.put(request.getProductId(), false);
				}
				
				// 如果是缓存刷新的请求,而且发现标识不为空,但是标识是false
				// 说明前面已经有一个数据库更新请求+一个缓存刷新请求了,大家想一想
				if(flag != null && !flag) {
					// 对于这种读请求,直接就过滤掉,不要放到后面的内存队列里面去了
					return;
				}
			}
			
			// 做请求的路由,根据每个请求的商品id,路由到对应的内存队列中去
			ArrayBlockingQueue<Request> queue = getRoutingQueue(request.getProductId());
			// 将请求放入对应的队列中,完成路由操作
			queue.put(request);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

7、空数据的读请求过滤优化

上面(6、)的代码:

				// 如果是缓存刷新的请求,而且发现标识不为空,但是标识是false
				// 说明前面已经有一个数据库更新请求+一个缓存刷新请求了,大家想一想
				if(flag != null && !flag) {
					// 对于这种读请求,直接就过滤掉,不要放到后面的内存队列里面去了
					return;
				}

8、其他

其实代码要跟业务结合,这里的代码未必100%正确,学根据业务自我调整 

如果读请求发现redis缓存中没有数据,就会发送读请求给库存服务,但是此时缓存中为空,可能是因为写请求先删除了缓存,也可能是数据库里压根儿没这条数据

如果是数据库中压根儿没这条数据的场景,那么就不应该将读请求操作给压入队列中,而是直接返回空就可以了

都是为了减少内存队列中的请求积压,内存队列中积压的请求越多,就可能导致每个读请求hang住的时间越长,也可能导致多个读请求被hang住

受不了了,上面的都太复杂了,有没有简单的总结一下?

 方案1:

 先删缓存,再更新数据库。同时辅助以“延时双删策略”      

 伪代码如下

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

转化为中文描述就是

  • (1)先淘汰缓存
  • (2)再写数据库(这两步和原来一样)
  • (3)休眠1秒,再次淘汰缓存 这么做,可以将1秒内所造成的缓存脏数据,再次删除。

参考:https://www.pianshen.com/article/4416270668/

          https://zhuanlan.zhihu.com/p/59167071

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值