亿级流量、高并发与高性能场景下的电商详情页架构_19(高并发场景下的缓存+数据库的双写不一致问题 解决方案 代码实现)

亿级流量、高并发与高性能场景下的电商详情页架构_19(高并发场景下的缓存+数据库的双写不一致问题 解决方案 代码实现)

大致思路:

1:更新数据时,根据数据的唯一标识、将操作路由之后,发送到一个jvm队列中去
2:读取数据时,如果数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由到同一jvm队列中去
3:每个线程依次地执行一个队列,待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中
4:如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值
在这里插入图片描述

步骤:

1、线程池+内存队列初始化

package com.fosu.eshop.inventory.thread;

import com.fosu.eshop.inventory.request.Request;
import com.fosu.eshop.inventory.request.RequestQueue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 线程池 单例
 * @author ITNTHX
 * @create 2021-04-16 12:59
 */
public class RequestProcessorThreadPool {

    // 在实际项目中,你设置线程池大小是多少,每个线程监控的那个内存队列的大小是多少
	// 都可以做到一个外部的配置文件中
    private ExecutorService threadPool = Executors.newFixedThreadPool(10);

    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));
        }
    }
    /**单例的实现*/
    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();
    }
}

内存队列初始化

package com.fosu.eshop.inventory.request;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 请求内存队列
 * @author ITNTHX
 * @create 2021-04-16 13:29
 */
public class RequestQueue {
    /**
     *内存队列List
     */
    private List<ArrayBlockingQueue<Request>> queues =
            new ArrayList<ArrayBlockingQueue<Request>>();

    /*创建一个单例的内存队列*/
    private static class Singleton{
        private  static RequestQueue instance;
        static {
            instance = new RequestQueue();
        }
        public static RequestQueue getInstance(){
            return instance;
        }
    }


    /**
     * 标识位map
     */
    private Map<Integer, Boolean> flagMap = new ConcurrentHashMap<Integer, Boolean>();


    /**
     * jvm的机制去保证多线程并发安全
     *
     * 内部类的初始化,一定只会发生一次,不管多少个线程并发去初始化
     *
     * @return
     */
    public static RequestQueue getInstance(){
        return Singleton.getInstance();
    }

    /*简单的初始化方法*/
    public static  RequestQueue init(){
        return getInstance();
    }


    public RequestQueue() {
        init();
    }

    /**
     * 添加一个内存队列
     * @param queue
     */
    public void addQueue(ArrayBlockingQueue<Request> queue){
        this.queues.add(queue);
    }

    /**
     * 获取内存队列List中内存队列的个数
     * @return size of queues
     */
    public int getQueueSize(){
        return this.queues.size();
    }

    /**
     * 获取内存队列List中对应的内存队列
     * @param index
     * @return
     */
    public ArrayBlockingQueue<Request> getQueue(int index){
        return this.queues.get(index);
    }

    public Map<Integer,Boolean> getFlagMap(){
        return flagMap;
    }

}


2、两种请求对象封装
请求对象1:ProductInventoryDBUpdateRequest

package com.fosu.eshop.inventory.request;

import com.fosu.eshop.inventory.model.ProductInventory;
import com.fosu.eshop.inventory.service.ProductInventoryService;
import com.fosu.eshop.inventory.service.impl.ProductInventoryServiceImpl;

/**
 * 比如说一个商品发生了交易,那么就要修改这个商品对应的库存
 *
 * 此时就会发送请求过来,要求修改库存,那么这个可能就是所谓的data update request,数据更新请求
 *
 *  cache aside pattern
 *
 * (1)删除缓存
 * (2)更新数据库
 * @author ITNTHX
 * @create 2021-04-19 18:22
 */
public class ProductInventoryDBUpdateRequest implements Request{

    /**
     * 商品库存
     */
    private ProductInventory productInventory;
    /**
     * 商品库存Service
     */
    private ProductInventoryService productInventoryService;

    public ProductInventoryDBUpdateRequest(ProductInventory productInventory,
                                           ProductInventoryServiceImpl productInventoryServiceImpl) {
        this.productInventory = productInventory;
        this.productInventoryService = productInventoryServiceImpl;
    }


    @Override
    public void process() {
        System.out.println("===========日志===========: 数据库更新请求开始执行,商品id=" + productInventory.getProductId() + ", 商品库存数量=" + productInventory.getInventoryCnt());
        // 删除redis中的缓存
        productInventoryService.removeProductInventoryCache(productInventory);
        // 更新数据库中的数据
        productInventoryService.updateProductInventory(productInventory);
    }

    @Override
    public Integer getProductId() {
        return productInventory.getProductId();
    }

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


请求对象2:ProductInventoryCacheRefreshRequest

package com.fosu.eshop.inventory.request;

import com.fosu.eshop.inventory.model.ProductInventory;
import com.fosu.eshop.inventory.service.ProductInventoryService;

/**
 * 重新加载商品库存的缓存
 * @author ITNTHX
 * @create 2021-04-19 18:23
 */

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);
    }

    @Override
    public Integer getProductId() {
        return productId;
    }

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

3、请求异步执行Service封装

package com.fosu.eshop.inventory.service.impl;

import com.fosu.eshop.inventory.dao.RedisDAO;
import com.fosu.eshop.inventory.mapper.ProductInventoryMapper;
import com.fosu.eshop.inventory.model.ProductInventory;
import com.fosu.eshop.inventory.service.ProductInventoryService;

import javax.annotation.Resource;

/**
 * @author ITNTHX
 * @create 2021-04-19 18:28
 */
public class ProductInventoryServiceImpl implements ProductInventoryService {

    @Resource
    private ProductInventoryMapper productInventoryMapper;
    @Resource
    private RedisDAO redisDAO;


    @Override
    public void updateProductInventory(ProductInventory productInventory) {
        productInventoryMapper.updateProductInventory(productInventory);
        System.out.println("===========日志===========: 已修改数据库中的库存,商品id=" + productInventory.getProductId() + ", 商品库存数量=" + productInventory.getInventoryCnt());
    }

    @Override
    public void removeProductInventoryCache(ProductInventory productInventory) {
        StringBuilder key = new StringBuilder("product:inventory:"+ productInventory.getProductId());
        redisDAO.delete(key.toString());
        System.out.println("===========日志===========: 已删除redis中的缓存,key=" + key);
    }

    /**
     * 根据商品id查询商品库存
     * @param productId 商品id
     * @return 商品库存
     */
    @Override
    public ProductInventory findProductInventory(Integer productId) {
        return productInventoryMapper.findProductInventory(productId);
    }

    /**
     * 设置商品库存的缓存
     * @param productInventory 商品库存
     */
    @Override
    public void setProductInventoryCache(ProductInventory productInventory) {
        StringBuilder key = new StringBuilder("product:inventory:" + productInventory.getProductId());
        redisDAO.set(key.toString(),productInventory.getInventoryCnt().toString());
        System.out.println("===========日志===========: 已更新商品库存的缓存,商品id=" + productInventory.getProductId() + ", 商品库存数量=" + productInventory.getInventoryCnt() + ", key=" + key);
    }


    @Override
    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;
    }
}

4、两种请求Controller接口封装

package com.roncoo.eshop.inventory.controller;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.roncoo.eshop.inventory.model.ProductInventory;
import com.roncoo.eshop.inventory.request.ProductInventoryCacheRefreshRequest;
import com.roncoo.eshop.inventory.request.ProductInventoryDBUpdateRequest;
import com.roncoo.eshop.inventory.request.Request;
import com.roncoo.eshop.inventory.service.ProductInventoryService;
import com.roncoo.eshop.inventory.service.RequestAsyncProcessService;
import com.roncoo.eshop.inventory.vo.Response;

/**
 * 商品库存Controller
 * @author Administrator
 * 
 * 大家考虑一下,我要模拟的场景:
 * 
 *(1)一个更新商品库存的请求过来,然后此时会先删除redis中的缓存,然后模拟卡顿5秒钟
 *(2)在这个卡顿的5秒钟内,我们发送一个商品缓存的读请求,因为此时redis中没有缓存,就会来请求将数据库中最新的数据刷新到缓存中
 *(3)此时读请求会路由到同一个内存队列中,阻塞住,不会执行
 *(4)等5秒钟过后,写请求完成了数据库的更新之后,读请求才会执行
 *(5)读请求执行的时候,会将最新的库存从数据库中查询出来,然后更新到缓存中
 *
 * 如果是不一致的情况,可能会出现说redis中还是库存为100,但是数据库中也许已经更新成了库存为99了
 * 
 * 现在做了一致性保障的方案之后,就可以保证说,数据是一致的
 * 
 * 最后说一点点
 * 
 * 包括这个方案在内,还有后面的各种解决方案,首先都是针对我自己遇到过的特殊场景去设计的
 * 
 * 可能这个方案就不一定完全100%适合其他的场景,也许还要做一些改造才可以,本来你学习一个课程,它就不是万能的,你可能需要嚼烂了,吸收了,改造了,才能应用到自己的场景中
 * 
 * 另外一个,也有一种可能,就是说方案比较复杂,即使我之前做过,也许有少数细节我疏忽了,没有在课程里面讲解,导致解决方案有一些漏洞或者bug
 * 
 * 我讲解方案,主要是讲解架构思想,或者是设计思想,技术思想,有些许漏洞,希望大家谅解
 * 
 * 课程真正最重要的,不是给你一套100%包打天下的代码,而是告诉一种设计思想,多种设计思想组合起来,就是某种架构思想
 * 
 */
@Controller
public class ProductInventoryController {

	@Resource
	private RequestAsyncProcessService requestAsyncProcessService;
	@Resource
	private ProductInventoryService productInventoryService;
	
	/**
	 * 更新商品库存
	 */
	@RequestMapping("/updateProductInventory")
	@ResponseBody
	public Response updateProductInventory(ProductInventory productInventory) {
		// 为了简单起见,我们就不用log4j那种日志框架去打印日志了
		// 其实log4j也很简单,实际企业中都是用log4j去打印日志的,自己百度一下
		System.out.println("===========日志===========: 接收到更新商品库存的请求,商品id=" + productInventory.getProductId() + ", 商品库存数量=" + productInventory.getInventoryCnt());
		
		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 {
			/*尝试从缓存中获取数据,如果有则直接返回,不必放入内存队列中去*/
			productInventory = productInventoryService.getProductInventoryCache(productId);
			if(productInventory != null) {
				System.out.println("===========日志===========: 在一开始,redis中的库存缓存,商品id=" + productInventory.getProductId() + ", 商品库存数量=" + productInventory.getInventoryCnt());
				return productInventory;
			}
			//重新加载商品缓存的请求
			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);  
	}
	
}

5、读请求去重优化

package com.fosu.eshop.inventory.thread;

import com.fosu.eshop.inventory.request.ProductInventoryCacheRefreshRequest;
import com.fosu.eshop.inventory.request.ProductInventoryDBUpdateRequest;
import com.fosu.eshop.inventory.request.Request;
import com.fosu.eshop.inventory.request.RequestQueue;
import org.apache.catalina.mapper.Mapper;

import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;

/**
 * 执行请求的工作线程
 * @author ITNTHX
 * @create 2021-04-16 13:22
 */
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 forceRefresh = request.isForceRefresh();

                //先做读请求的去重
                if(!forceRefresh){
                    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;
    }
}

6、空数据读请求过滤优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值