实现缓存与数据库双写一致性保障
pox文件:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.2.5.RELEASE</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.8</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.1.43</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestone</id> <url>https://repo.spring.io/libs-release</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-milestone</id> <url>https://repo.spring.io/libs-release</url> </pluginRepository> </pluginRepositories>
Application:
import java.util.HashSet; import java.util.Set; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.tomcat.jdbc.pool.DataSource; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.embedded.ServletListenerRegistrationBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import com.roncoo.eshop.inventory.listener.InitListener; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; @EnableAutoConfiguration //自动载入应用程序所需的所有Bean @SpringBootApplication //启动的类 @ComponentScan //扫描包 @MapperScan("com.roncoo.eshop.inventory.mapper") //启动入口函数 public class Application { //构建数据源 @Bean @ConfigurationProperties(prefix="spring.datasource") public DataSource dataSource() { return new DataSource(); } //构建MyBatis的入口类:SqlSessionFactory @Bean public SqlSessionFactory sqlSessionFactoryBean1() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource()); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/mybatis/*.xml")); return sqlSessionFactoryBean.getObject(); } //构建事务管理器 @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dataSource()); } @Bean public JedisCluster JedisClusterFactory() { Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>(); jedisClusterNodes.add(new HostAndPort("192.168.31.19", 7003)); jedisClusterNodes.add(new HostAndPort("192.168.31.19", 7004)); jedisClusterNodes.add(new HostAndPort("192.168.31.227", 7006)); JedisCluster jedisCluster = new JedisCluster(jedisClusterNodes); return jedisCluster; } //注册监听器 线程池+内存队列初始化 @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public ServletListenerRegistrationBean servletListenerRegistrationBean(){ ServletListenerRegistrationBean servletListenerRegistrationBean= new ServletListenerRegistrationBean(); //添加listener servletListenerRegistrationBean.setListener(new InitListener()); return servletListenerRegistrationBean; } //SpringBoot 启动入口的方法 public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import com.roncoo.eshop.inventory.thread.RequestProcessorThreadPool; //系统初始化监听器 public class InitListener implements ServletContextListener{ @Override public void contextInitialized(ServletContextEvent sce){ // 初始化工作线程池和内存队列 RequestProcessorThreadPool.init(); } @Override public void contextDestroyed(ServletContextEvent sce) { } }
public interface Request { void process(); Integer getProductId(); boolean isForceRefresh(); }
请求队列:
import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentHashMap; //请求内存队列 public class RequestQueue { //内存队列 private List<ArrayBlockingQueue<Request>> queues= new ArrayList<ArrayBlockingQueue<Request>>(); //标示位map private Map<Integer, Boolean> flagMap =new ConcurrentHashMap<Integer,Boolean>(); //采用线程安全的方式实现单例 //静态内部类的方法,去初始化单例 private static class Singleton{ private static RequestQueue instance; static{ instance=new RequestQueue(); } public static RequestQueue getInstance(){ return instance; } } //jvm的机制,保证多线程并发安全 //内部类的初始化 ,一定只发生一次,不管多少个线程并发去初始化 public static RequestQueue getInstance(){ return Singleton.getInstance(); } //添加一个内存队列 public void addQueue(ArrayBlockingQueue<Request> queue){ this.queues.add(queue); } //获取内存队列的数量 public int queueSize(){ return queues.size(); } //获取内存队列 public ArrayBlockingQueue<Request> getQueue(int index){ return queues.get(index); } public Map<Integer, Boolean> getFlagMap(){ return flagMap; } }
执行请求的工作线程:
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Callable; 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.request.RequestQueue; import java.util.Map; //执行请求的工作线程 public class RequestProcessorThread implements Callable<Boolean> { //自己监控的内存队列 private ArrayBlockingQueue<Request> queue; //初始化的方法 public RequestProcessorThread(ArrayBlockingQueue<Request> queue) { this.queue = queue; } //请求处理封装 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; } } } // 执行这个request操作 request.process(); } } catch (Exception e) { e.printStackTrace(); } return true; } }
请求处理的线程池:
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import com.roncoo.eshop.inventory.request.Request; import com.roncoo.eshop.inventory.request.RequestQueue; //请求处理线程池:单例 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)); } } //静态内部类的方式去初始化线程的方式 public static class Singleton{ private static RequestProcessorThreadPool instance; static{ instance=new RequestProcessorThreadPool(); } public static RequestProcessorThreadPool getInstance(){ return instance; } } //jvm的方式去保证多线程并发安全 //内部类的初始化,一定只发生一次,不管多少个线程并发初始化 public static RequestProcessorThreadPool getInstance(){ return Singleton.getInstance(); } //初始化的便捷方法 public static void init(){ getInstance(); } }
请求的响应:
//请求的响应 public class Response { public static final String SUCCESS = "success"; public static final String FAILURE = "failure"; private String status; private String message; public Response() { } public Response(String status) { this.status = status; } public Response(String status, String message) { this.status = status; this.message = message; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
2、两种请求对象封装
import com.roncoo.eshop.inventory.model.ProductInventory; import com.roncoo.eshop.inventory.service.ProductInventoryService; /** * 比如说一个商品发生了交易,那么就要修改这个商品对应的库存 * 此时就会发送请求过来,要求修改库存,那么这个可能就是所谓的data update request,数据更新请求 * cache aside pattern * (1)删除缓存 * (2)更新数据库 * **/ //请求对象的封装,删除redis的缓存,修改数据库中的库存 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(){ // 删除redis中的缓存 productInventoryService.removeProductInventoryCache(productInventory); // 修改数据库中的库存 productInventoryService.updateProductInventory(productInventory); } //获取商品id public Integer getProductId(){ return productInventory.getProductId(); } @Override public boolean isForceRefresh() { return false; } }
import com.roncoo.eshop.inventory.model.ProductInventory; import com.roncoo.eshop.inventory.service.ProductInventoryService; //重新加载商品库存的缓存(请求对象的封装) 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); // 将最新的商品库存数量,刷新到redis缓存中去 productInventoryService.setProductInventoryCache(productInventory); } //返回商品的id public Integer getProductId() { return productId; } public boolean isForceRefresh() { return forceRefresh; } }
3、请求异步执行Service封装
import java.util.concurrent.ArrayBlockingQueue; import org.springframework.stereotype.Service; import com.roncoo.eshop.inventory.request.Request; import com.roncoo.eshop.inventory.request.RequestQueue; import com.roncoo.eshop.inventory.service.RequestAsyncProcessService; //请求异步处理的service实现 @Service("requestAsyncProcessService") public class RequestAsyncProcessServiceImpl implements RequestAsyncProcessService{ //做请求的的路由,根据每个请求的商品id,路由到对应的内存队列中 @Override public void process(Request request) { try { // 做请求的路由,根据每个请求的商品id,路由到对应的内存队列中去 ArrayBlockingQueue<Request> queue = getRoutingQueue(request.getProductId()); // 将请求放入对应的队列中,完成路由操作 queue.put(request); } catch (Exception e) { e.printStackTrace(); } } //获取路由到的内存队列 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; return requestQueue.getQueue(index); } }
import javax.annotation.Resource; import org.springframework.stereotype.Service; import com.roncoo.eshop.inventory.dao.RedisDAO; import com.roncoo.eshop.inventory.mapper.ProductInventoryMapper; import com.roncoo.eshop.inventory.model.ProductInventory; import com.roncoo.eshop.inventory.service.ProductInventoryService; //商品库存Service实现类 @Service("productInventoryService") public class ProductInventoryServiceImpl implements ProductInventoryService { @Resource private ProductInventoryMapper productInventoryMapper; @Resource private RedisDAO redisDAO; @Override public void updateProductInventory(ProductInventory productInventory) { productInventoryMapper.updateProductInventory(productInventory); } @Override public void removeProductInventoryCache(ProductInventory productInventory){ String key="product:inventory:"+productInventory.getProductId(); redisDAO.delete(key); } //根据商品id查询商品库存 public ProductInventory findProductInventory(Integer productId) { return productInventoryMapper.findProductInventory(productId); } //设置商品库存的缓存 public void setProductInventoryCache(ProductInventory productInventory){ String key = "product:inventory:" + productInventory.getProductId(); redisDAO.set(key, String.valueOf(productInventory.getInventoryCnt())); } // 获取商品库存的缓存 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; } }
import com.roncoo.eshop.inventory.request.Request; //请求异步执行的server public interface RequestAsyncProcessService { void process(Request request); }
import com.roncoo.eshop.inventory.model.ProductInventory; 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); }
控制器:
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 /* *(1)一个更新商品库存的请求过来,然后此时会先删除redis中的缓存,然后模拟卡顿5秒钟 *(2)在这个卡顿的5秒钟内,我们发送一个商品缓存的读请求,因为此时redis中没有缓存,就会来请求将数据库中最新的数据刷新到缓存中 *(3)此时读请求会路由到同一个内存队列中,阻塞住,不会执行 *(4)等5秒钟过后,写请求完成了数据库的更新之后,读请求才会执行 *(5)读请求执行的时候,会将最新的库存从数据库中查询出来,然后更新到缓存中 * */ public class 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){ ProductInventory productInventory = null; try{ Request request = new ProductInventoryCacheRefreshRequest( productId, productInventoryService, false); requestAsyncProcessService.process(request); requestAsyncProcessService.process(request); // 将请求扔给service异步去处理以后,就需要while(true)一会儿,在这里hang住 // 去尝试等待前面有商品库存更新的操作,同时缓存刷新的操作,将最新的数据刷新到缓存中 long startTime = System.currentTimeMillis(); long endTime = 0L; long waitTime = 0L; // 等待超过200ms没有从缓存中获取到结果 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) { // 将缓存刷新一下 productInventoryService.setProductInventoryCache(productInventory); return productInventory; } } catch (Exception e) { e.printStackTrace(); } return new ProductInventory(productId, -1L); } }
public interface RedisDAO { void set(String key, String value); String get(String key); void delete(String key); }
import javax.annotation.Resource; import org.springframework.stereotype.Repository; import redis.clients.jedis.JedisCluster; import com.roncoo.eshop.inventory.dao.RedisDAO; @Repository("redisDAO") public class RedisDAOImpl implements RedisDAO { @Resource private JedisCluster jedisCluster; @Override public void set(String key, String value) { jedisCluster.set(key, value); } @Override public String get(String key) { return jedisCluster.get(key); } @Override public void delete(String key) { jedisCluster.del(key); } }
6、读请求去重优化
如果一个读请求过来,发现前面已经有一个写请求和一个读请求了,那么这个读请求就不需要压入队列中了
因为那个写请求肯定会更新数据库,然后那个读请求肯定会从数据库中读取最新数据,然后刷新到缓存中,自己只要hang一会儿就可以从缓存中读到数据了
7、空数据读请求过滤优化
可能某个数据,在数据库里面压根儿就没有,那么那个读请求是不需要放入内存队列的,而且读请求在controller那一层,直接就可以返回了,不需要等待
如果数据库里都没有,就说明,内存队列里面如果没有数据库更新的请求的话,一个读请求过来了,就可以认为是数据库里就压根儿没有数据吧
如果缓存里没数据,就两个情况,第一个是数据库里就没数据,缓存肯定也没数据; 第二个是数据库更新操作过来了,先删除了缓存,此时缓存是空的,但是数据库里是有的
但是的话呢,我们做了之前的读请求去重优化,用了一个flag map,只要前面有数据库更新操作,flag就肯定是存在的,你只不过可以根据true或false,判断你前面执行的是写请求还是读请求
但是如果flag压根儿就没有呢,就说明这个数据,无论是写请求,还是读请求,都没有过
那这个时候过来的读请求,发现flag是null,就可以认为数据库里肯定也是空的,那就不会去读取了
或者说,我们也可以认为每个商品有一个最最初始的库存,但是因为最初始的库存肯定会同步到缓存中去的,有一种特殊的情况,就是说,商品库存本来在redis中是有缓存的
但是因为redis内存满了,就给干掉了,但是此时数据库中是有值得
那么在这种情况下,可能就是之前没有任何的写请求和读请求的flag的值,此时还是需要从数据库中重新加载一次数据到缓存中的