• 01.分布式数据库和缓存一致性问题
  • 发布于 1个月前
  • 38 热度
    0 评论

写这篇文章的背景因素:

1. 秋招面试好未来第三批,二面莫名其妙挂掉。因为实习没注意要秋招了,都是泪。

2. 面试过程被面试官问到是否遇到数据库和缓存不一致的问题。秉着诚实不做作的态度,说没有遇到。但是没有解释为什么没有遇到的原因。是因为自己项目的并发量没测到还是因为没考虑到还是因为根本没注意过这个问题。所以感觉自己很失败。

3. 自己Redis技术太菜,只好写总结了。


引言

项目中用到了redis作缓存,之前做的时候没有考虑到更新缓存的时候的策略。只是随手的Google了一下便去写代码了。这里给自己一个警告。不注意的地方日后都是大坑。所以趁着最近笔试和面试少了,将缓存和数据库在分布式情况下如何保持一致性做一个总结。

正文


1. 读取缓存的流程:

对于读取缓存谷歌后的结果大致都是一样。都是这样做判断的。


2.但是对于修改缓存网上的做法很多。主要参考这里:http://blog.jobbole.com/113992/。很全。

对于自己的项目中关于修改缓存这一方面一开始做的时候没有想太多,所以就谷歌了一下大家的做法就去做了,也没有思考为什么,所以二面被问到也是个难受。贴代码自己总结。

自己的做法:先修改数据库后删除缓存。

优点:存在并发的问题概率很小很小,因为数据库的写操作远远慢于读操作,这也是读写分离的原因之一。

缺点:仍然会发生脏读现象。使得数据库与缓存不一致。

解释:线程1做查询,线程2做修改。

          <1>. 缓存此时失效。所以去查数据库的数据。

          <2>. 线程1去查数据库。

          <3>. 线程2对数据库做写操作。

          <4>. 线程2开始去删除缓存的内容。

          <5>. 线程1将刚才查到的数据库内容写入缓存中。

此时线程2已经写了数据库,数据库的内容此时和1拿到的数据不一致,所以出现了脏数据。但是概率上这种情况发生的可能性很小很小。只有线程2写数据库的速度远远快于线程1去查数据库的速度的时候,才会导致线程1拿到的数据是脏数据。但是对于数据库来说,读写分离的原因就在于读操作比较多而且远远快于写操作,所以发生这种事情的概率很小。


2.1 解决方法:

很小的概率不代表不会发生,所以还是要去做保障。基于此当然要想办法。解决方法如上博客。


3. 其他导致不一致原因:

再对缓存操作的时候,我们总是为缓存操作加上异常处理,因为缓存的操作不可以影响到业务逻辑的进行。所以另外一种可能导致缓存与数据不一致原因就是:操作缓存出现不可避免的异常情况了。刮风打雷下雨闪电都可能会导致计算机中的某一个01序列变化,检错机制不能完全保证代码的运行,我们无法避免,只能降低风险。

贴上项目中的某一段代码:

Date date = new Date();
		content.setCreated(date);
		content.setUpdated(date);
		int num = tbContentMapper.insert(content);
		if(num < 1) {
			throw new RuntimeException("数据库异常,请查看日志!");
		}
		try {
			jedisClient.hdel("CONTENT_KEY", content.getCategoryId()+"");
		} catch (Exception e) {
			//删除失败
			//transferString(content.getCategoryId()+"");
			jmsTemplate.send(destination,new MessageCreator() {
				@Override
				public Message createMessage(Session session) throws JMSException {
					return session.createTextMessage(content.getCategoryId()+"");
				}
			});
                   //其他业务逻辑
		}
项目使用的是ActiveMq作为消息中间件。可以看到项目中自己采用的是先修改数据库后删除缓存。在try catch 块内做异常处理。

异常处理的方法就是:当出现不可避免的错误的时候使用消息中间件来发送消息,此时自己既是生产造也是消费者。因为消息自己消费了。提升异常的级别为Exception。此时只要出现异常仍然还是继续做此操作。


代码的逻辑:将要删除的数据的key发送至自己然后看下面的代码逻辑。

//内部类仅仅服务这一个类
	class FailLisetener implements MessageListener {
		TextMessage textMessage = null;
		String categoryId = null;
		@Override
		public void onMessage(Message message) {
			try {
				if(message instanceof TextMessage) {
					textMessage = (TextMessage) message;
					categoryId= textMessage.getText();
				}
				jedisClient.hdel("CONTENT_KEY", categoryId);
			} catch (JMSException e) {
				//打日志。此时打日志即可。
			}
		}

这里仍然是自己作为消费者。将消息消化即可。如果出现异常就去消化消息,重新删除即可。直到删除成功。但是出现异常的情况的概率小到害怕,所以这里的异常没有做处理。打日志即可。后期便于排查。


总结:

数据库和缓存不一致的原因:多线程并发访问对于修改缓存采取的方案不同出现的原因也不一样。

解决方法:确定修改缓存的方案,考虑所有可能会出现的情况。解决其方法即可。


用户评论