在实际业务中,经常碰见数据库和缓存中数据不一致的问题,缓存作为抵挡前端访问洪峰的工具,用的好的话可以大大减轻服务端压力,但是在一些场景下,如果没有控制好很容易造成数据库和缓存的数据不一致性,尤其是在并发环境下,这种情况出现的概率会大大的增加,为什么会出现这个问题呢?我们来简单分析一下。

1、页面发起一个请求,请求更新某个商品的库存,正常的流程是,如果没有缓存,直接更新数据库即可,如果有缓存,先删除该商品在redis中的缓存,然后再去更新数据库,同时可能还把更新后的库存数据同步到缓存中,这是正常的流程,也是基本上保证数据库和缓存数据一致性最基本的方式;

2、假如还是第一个场景的描述,这时候因为某种原因,比如网络中断一会儿,或者数据库连接异常导致在删除缓存的那一瞬间,正好有一个读取商品库存的请求发过来,而且正好这时候环境恢复正常,由于这两次操作可能是在分布式的环境下进行的,各自归属不同的事物互不影响,那么这时候问题就来了,读请求读到的数据还是老数据,而实际上应该是读到上一个请求更新后的库存数据,这样就造成了数据的不一致性,这样的场景在高并发环境下应该很容易模拟出来;

常见的解决思路在上面的描述中已经提到过一种,即先删除缓存在更新数据库,但这是在普通的环境下,但是在场景2中该如何解决呢?

我们可以设想,假如说有这样一种方式,对相同的数据进行操作,比如某商品的productId,或者订单orderId,我们是否可以让前一个写请求没有完成的情况下,后面的读请求被阻塞住那么一段时间,等待前面的写操作执行完毕后再进行读操作呢?

理论上来说是可行的,我们可以通过某种方式,比如阻塞队列,让相同的数据进入相同的队列,阻塞队列就有很好的顺序执行保障机制,保证入队的数据顺序执行,这样一来,是不是就可以达到目的呢?

说白了,就是前端发来一个的请求,若是写请求,将这个请求入队,然后从队列中取出请求执行写的业务逻辑操作,如果是读请求而且是相同的数据,我们通过自定义的想过路由算法路由到相同的队列中,通过设定一定的等待时间来达到让这两个请求由并行操作变成串行操作,这不就达到目的了吗?

按照这个思路,我们来解决下面的业务需求。

第一个请求,更新数据库的商品缓存,这时候,又一个请求过来,要读取这个商品的库存

通过以上的分析思路,我们将解决的代码思路整理如下,

1、初始化一个监听器,在程序启动的时候将针对不同请求的队列全部创建出来;
2、封装两种不同的请求对象,一种是更新缓存,一种是读取数据并刷新到缓存;
3、处理两种不同请求的异步路由service;
4、两种请求的controller;
5、读请求去重优化;
6、空数据读请求过滤优化

根据以上思路,我们来整合一下这个过程,

1、项目结构,

2、pom依赖文件,主要是springboot的相关依赖,

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.1.RELEASE</version><relativePath /> <!-- lookup parent from repository --></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</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-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- rabbitmq的依赖包 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><!-- 数据库连接池 --><!-- <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.9</version> </dependency> --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency><!-- http所需包 --><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpcore</artifactId></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpmime</artifactId></dependency><!-- /http所需包 --><!--添加fastjson依赖 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.7</version></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version><!--$NO-MVN-MAN-VER$ --></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.apache.tomcat</groupId><artifactId>tomcat-jdbc</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>

3、整合mybatis以及数据库的连接,这里采用的是程序加载bean的方式配置,

/*** 整合mybatis以及数据库连接配置* @author asus**/
@Component
public class DataSourceConfig {@Bean@ConfigurationProperties(prefix="spring.datasource")public DataSource dataSource(){return new DataSource();}@Beanpublic SqlSessionFactory sqlSessionFactory() throws Exception{SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSource());PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/mybatis/*.xml"));return sqlSessionFactoryBean.getObject();}    @Beanpublic PlatformTransactionManager transactionManager(){return new DataSourceTransactionManager(dataSource());}}

数据库配置信息:application.properties

server.port=8085spring.datasource.url=jdbc:mysql://localhost:3306/babaytun?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=rootspring.redis.database=3
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0spring.redis.jedis.pool.max-active=100
spring.redis.jedis.pool.max-idle=100
spring.redis.jedis.pool.min-idle=10
spring.redis.jedis.pool.max-wait=50000ms

4、初始化监听器,通过这个监听器,我们把要操作的内存队列全部创建出来,

/*** 系统初始化监听器* @author asus**/
public class InitListener implements ServletContextListener{@Overridepublic void contextInitialized(ServletContextEvent sce) {System.out.println("系统初始化 =============================");RequestProcessorThreadPool.init();}@Overridepublic void contextDestroyed(ServletContextEvent sce) {}}

可以看到,在监听器里面有一个RequestProcessorThreadPool.init(); 的方法,通过调用这个方法就可以实现创建内存队列的行为,我们就从这个类看起,

5、初始化一个可存入10个线程的线程池,往每个线程里塞满相应的阻塞队列,每个队列里最大可处理数量为100个,可以自定义,为了保证线程安全,这里采用静态内部类的方式,

/*** 请求处理线程池:单例*/
public class RequestProcessorThreadPool {// 实际项目中,你设置线程池大小是多少,每个线程监控的那个内存队列的大小是多少// 都可以做到一个外部的配置文件中/*** 初始化一个可存入10个线程的线程池,往每个线程里塞满10个*/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));  }}/*** 采取绝对线程安全的一种方式* 静态内部类的方式,去初始化单例**/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();}}
/*** 请求内存队列**/
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;}}

6、接下来,我们来封装具体的请求,一个是请求更新数据库的,一个是读取请求,

【1】更新数据库数据请求:

/*** 比如说一个商品发生了交易,那么就要修改这个商品对应的库存* 此时就会发送请求过来,要求修改库存,那么这个可能就是所谓的data update request,数据更新请求* (1)删除缓存* (2)更新数据库*/
public class ProductInventoryDBUpdateRequest implements Request {private static final Logger logger = LoggerFactory.getLogger(RequestAsyncProcessServiceImpl.class);/*** 商品库存*/private ProductInventory productInventory;/*** 商品库存Service*/private ProductInventoryService productInventoryService;public ProductInventoryDBUpdateRequest(ProductInventory productInventory,ProductInventoryService productInventoryService) {this.productInventory = productInventory;this.productInventoryService = productInventoryService;}@Overridepublic void process() {logger.info("===========日志===========: "+ "数据库更新请求开始执行,商品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();}@Overridepublic boolean isForceRefresh() {return false;}}

【2】读取最新数据请求:

/*** 重新加载商品库存的缓存**/
public class ProductInventoryCacheRefreshRequest implements Request {private static final Logger logger = LoggerFactory.getLogger(RequestAsyncProcessServiceImpl.class);/*** 商品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;}@Overridepublic void process() {// 从数据库中查询最新的商品库存数量ProductInventory productInventory = productInventoryService.findProductInventory(productId);logger.info("===========日志===========: 已查询到商品最新的库存数量,商品id=" + productId + ", 商品库存数量=" + productInventory.getInventoryCnt());  // 将最新的商品库存数量,刷新到redis缓存中去productInventoryService.setProductInventoryCache(productInventory); }public Integer getProductId() {return productId;}public boolean isForceRefresh() {return forceRefresh;}}

7、两种请求的具体业务逻辑实现类,

【1】更新商品数据,

/*** 修改商品库存Service实现类**/
@Service("productInventoryService")
public class ProductInventoryServiceImpl implements ProductInventoryService {private static final Logger logger = LoggerFactory.getLogger(ProductInventoryController.class);@Resourceprivate ProductInventoryMapper productInventoryMapper;@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 修改数据库商品库存数量*/public void updateProductInventory(ProductInventory productInventory) {productInventoryMapper.updateProductInventory(productInventory); logger.info("===========日志===========: 已修改数据库中的库存,商品id=" + productInventory.getProductId() + ", 商品库存数量=" + productInventory.getInventoryCnt());}/*** 根据商品的id删除redis中某商品*/public void removeProductInventoryCache(ProductInventory productInventory) {String key = "product:inventory:" + productInventory.getProductId();//redisTemplate.delete(key);redisTemplate.delete(key);logger.info("===========日志===========: 已删除redis中的缓存,key=" + key); }/*** 根据商品id查询商品库存* @param productId 商品id * @return 商品库存*/public ProductInventory findProductInventory(Integer productId) {return productInventoryMapper.findProductInventory(productId);}/*** 设置商品库存的缓存* @param productInventory 商品库存*/public void setProductInventoryCache(ProductInventory productInventory) {String key = "product:inventory:" + productInventory.getProductId();//redisTemplate.opsForValue().set(key, String.valueOf(productInventory.getInventoryCnt()));redisTemplate.opsForValue().set(key, String.valueOf(productInventory.getInventoryCnt()));logger.info("===========日志===========: 已更新商品库存的缓存,商品id=" + productInventory.getProductId() + ", 商品库存数量=" + productInventory.getInventoryCnt() + ", key=" + key);  }/*** 获取商品库存的缓存* @param productId* @return*/public ProductInventory getProductInventoryCache(Integer productId) {Long inventoryCnt = 0L;String key = "product:inventory:" + productId;//String result = (String) redisTemplate.opsForValue().get(key);String result = (String) redisTemplate.opsForValue().get(key);if(result != null && !"".equals(result)) {try {inventoryCnt = Long.valueOf(result);return new ProductInventory(productId, inventoryCnt);} catch (Exception e) {e.printStackTrace(); }}return null;}}

【2】读取商品数据,

/*** 请求读取数据异步处理的service实现**/
@Service("requestAsyncProcessService")
public class RequestAsyncProcessServiceImpl implements RequestAsyncProcessService {private static final Logger logger = LoggerFactory.getLogger(RequestAsyncProcessServiceImpl.class);@Overridepublic 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;logger.info("===========日志===========: 路由内存队列,商品id=" + productId + ", 队列索引=" + index);  return requestQueue.getQueue(index);}}

在请求读取最新的商品库存数据方法中,有个根据商品的id路由到对应的内存队列的方法,即相同的productId通过这个路由方法之后会进入某个线程下相同的arrayBlockingQueue中,这样就能保证读写的顺序性执行,

8、在初始化创建线程池的方法中,我们注意到有一个方法,即我们把整个queue提交到线程池里面了,其实在这个RequestProcessorThread 就是具体执行请求路由转发的,也就是说,一个前端请求打过来,只要是我们定义的请求类型,就会执行这个里面的转发逻辑,

9、以上就是主要的业务封装类,下面就来看看controller中两种请求的方法类,

/*** 商品库存Controller* 要模拟的场景:* *(1)一个更新商品库存的请求过来,然后此时会先删除redis中的缓存,然后模拟卡顿5秒钟*(2)在这个卡顿的5秒钟内,我们发送一个商品缓存的读请求,因为此时redis中没有缓存,就会来请求将数据库中最新的数据刷新到缓存中*(3)此时读请求会路由到同一个内存队列中,阻塞住,不会执行*(4)等5秒钟过后,写请求完成了数据库的更新之后,读请求才会执行*(5)读请求执行的时候,会将最新的库存从数据库中查询出来,然后更新到缓存中* 如果是不一致的情况,可能会出现说redis中还是库存为100,但是数据库中也许已经更新成了库存为99了* 现在做了一致性保障的方案之后,就可以保证说,数据是一致的* * */
@Controller
public class ProductInventoryController {private static final Logger logger = LoggerFactory.getLogger(ProductInventoryController.class);@Resourceprivate RequestAsyncProcessService requestAsyncProcessService;@Resourceprivate ProductInventoryService productInventoryService;/*** 更新商品库存*/@RequestMapping("/updateProductInventory")@ResponseBodypublic Response updateProductInventory(ProductInventory productInventory) {// 为了简单起见,我们就不用log4j那种日志框架去打印日志了// 其实log4j也很简单,实际企业中都是用log4j去打印日志的,自己百度一下logger.info("===========日志===========: 接收到更新商品库存的请求,商品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")@ResponseBodypublic ProductInventory getProductInventory(Integer productId) {logger.info("===========日志===========: 接收到一个商品库存的读请求,商品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) {logger.info("===========日志===========: 在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);  }}

为了模拟出鲜果,我在读取数据的方法里面加了个wait的操作,这里wait的时间要大于请求更新数据库的时间长,因为要确保写请求执行完毕才执行读请求,这样读取到的数据才是最新的数据,就达到了阻塞队列的实际作用,

9、其他的类这里略过了,比较简单,主要是接口、实体类和一个更新数据库数据和查询的,注意,我们配置了监听器,在springboot里面一定要将监听器作为bean配置到启动类里面,否则不生效,

@EnableAutoConfiguration
@SpringBootApplication
@Component
@MapperScan("com.congge.mapper")
public class MainApp {/*** 注册web应用启动监听器bean* @return*/@SuppressWarnings("rawtypes")@Beanpublic ServletListenerRegistrationBean servletListenerRegistrationBean(){ServletListenerRegistrationBean<InitListener> listenerRegistrationBean = new ServletListenerRegistrationBean<InitListener>();listenerRegistrationBean.setListener(new InitListener());return listenerRegistrationBean;}public static void main(String[] args) {SpringApplication.run(MainApp.class, args);}}

9、数据库中我提前创建好了一张表,并在redis中初始化了一条和数据库相同的数据,

启动项目,首先我们访问,即发起请求,将这条数据的库存变为99,
http://localhost:8085/updateProductInventory?productId=1&inventoryCnt=99

同时立即发起请求,即读取这条数据的库存,即模拟一个并发操作,
http://localhost:8085/getProductInventory?productId=1

总体的效果是,第一个更新请求立即访问,由于后台有wait,所以相应的是null,当过了20秒后,我们继续刷新,更新成功,再发起读取数据请求,第二个请求相应到的数据就是更新完毕的数据99,中间第二个请求会一直卡顿在那儿,



同时可以看到后台的打印日志,也是按照我们的执行顺序进行的,

这样就达到了我们的目的,通过上述整合,我们实现了模拟在并发环境下实现mysql和redis中数据读写的一致性,本篇到此结束,感谢观看!

高并发场景下的缓存 + 数据库双写不一致问题分析与解决方案设计相关推荐

  1. 读数据库遇到空就进行不下去_如何解决高并发场景下缓存+数据库双写不一致问题?...

    推荐阅读: 一只Tom猫:手撕分布式技术:限流.通讯.缓存,全部一锅端走送给你!​zhuanlan.zhihu.com 一只Tom猫:MySQL复习:20道常见面试题(含答案)+21条MySQL性能调 ...

  2. 高并发场景下的缓存有哪些常见的问题?

    作者 l 丁码农 来源:https://www.cnblogs.com/dinglang 一.缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副 ...

  3. 高并发场景下数据库的常见问题及解决方案

    一.分库分表 (1)为什么要分库分表 随着系统访问量的增加,QPS越来越高,数据库磁盘容量不断增加,一般数据库服务器的QPS在800-1200的时候性能最佳,当超过2000的时候sql就会变得很慢并且 ...

  4. 并发经验八年架构师:缓存在高并发场景下该如何问题

    缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象.这就比较依赖缓存的过期和更新策略.一般会在数据发生更改 ...

  5. 高并发场景下缓存的常见问题

    作者介绍: 丁浪,非著名架构师.关注高并发.高可用的架构设计,对系统服务化.分库分表.性能调优等方面有深入研究和丰富实践经验.热衷于技术研究和分享. 声明:版权归丁浪作者本人所有,转载请联系作者本人 ...

  6. 本地缓存需要高时效性怎么办_缓存在高并发场景下的常见问题

    缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象.这就比较依赖缓存的过期和更新策略.一般会在数据发生更改 ...

  7. java高并发(二十一)高并发场景下缓存常见问题

    缓存一致性 当数据实时性要求很高时,需要保证缓存中的数据与数据库中的数据一致,缓存节点与副本中的数据一致,不能出现差异现象,这就比较依赖缓存的过期和更新策略了.一般会在数据发生更改的时候,主动跟新缓存 ...

  8. 并发经验八年架构师:带你轻松解决缓存在高并发场景下的问题

    缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象.这就比较依赖缓存的过期和更新策略.一般会在数据发生更改 ...

  9. 缓存在高并发场景下的常见问题

    缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象.这就比较依赖缓存的过期和更新策略.一般会在数据发生更改 ...

最新文章

  1. CSS 后台布局实例
  2. Linux下修改mysql默认最大连接数,查看当前设置的最大连接数
  3. 多媒体-设备的名称已被此应用程序用作别名,请使用唯一的别名
  4. 基于Python的BPSK音频的波形和频谱
  5. android 请求参数打印,android retrofit 请求参数格式RequestBody的方法
  6. 实验13 简单FTP 程序设计
  7. C语言 strnlen函数实现
  8. linux中匿名用户怎么登陆_Linux网络配置 | FTP 实战-匿名用户登录
  9. 七种武器武装.NET(常用开发工具介绍)(转)
  10. 一台机器上安装两个tomcat
  11. java多线程图解_java多线程实例图解讲解
  12. 【NLP】EMNLP 2019 参会小结及最佳论文解读
  13. MESOS集群高可用部署
  14. 微信小程序安装moment报错VM182 WAService.js:2 Error: module “pages/mine/myWish/detail/moment.js“ is not define
  15. 语音合成IC与语音IC的两三事
  16. linux文件权限651,Linux基础之文件权限详解
  17. win10禁用笔记本键盘
  18. Java和Spring:发送邮件(以QQ邮箱为例)
  19. 计算机软件的卸载,电脑里那些软件是不需要的?应该怎么卸载?
  20. 程序员工资的7个段位,你的工资在哪个Level?

热门文章

  1. DirectX 基础学习系列5 纹理映射
  2. kafka分区停留在UnderReplicated状态
  3. wireshark 抓 grpc 包
  4. PyQt5 Pyinstaller时出现错误Cannot find PyQt5 plugin directories
  5. 深入分析Java ClassLoader原理
  6. Android IOS WebRTC 音视频开发总结(六二)-- 大数据解密国外实时通讯行业开发现状...
  7. linux下udp多线程编程
  8. 《认清C++语言》之--内存管理
  9. 反流氓软件运动为何变了味
  10. JConsole工具使用