文章目录

  • 缓存问题
  • 缓存穿透
  • 缓存击穿
  • 缓存雪崩
  • 缓存与数据库一致性


缓存问题

缓存穿透

如果要获取的数据在缓存和数据库中都不存在,就可能导致每次获取该数据都需要查询数据库,造成不必要的开销。

解决方案一:缓存空值

如果数据库中没有的数据也为其缓存一个空值,那么就可以避免缓存穿透问题。

public void getMessage(Long id) {
	// 查询缓存中是否有key,无论value是否为null都返回结果。
	Message message = null;
	if (jedis.exists(id.toString())) {
		message = JSONObject.parseObject(jedis.get(id.toString()), Message.class);
		System.out.println("缓存数据:" + message);
		return;
	}
	// 如果缓存中没有key,说明数据未被缓存,开始查询数据库。
	try (SqlSession session = sqlSessionFactory.openSession();) {
		MessageService messageService = session.getMapper(MessageService.class);
		message = messageService.getMessageById(id);
		jedis.set(id.toString(), JSONObject.toJSONString(message));
		System.out.println("数据库数据" + message);
	}
}

缓存击穿

如果某数据还未被缓存,并有多个线程同时获取该数据,这些线程就可能同时开始查询数据库,造成额外开销。

解决方案

通过互斥锁等串行化机制,使得同时只能有一个线程进行数据库的访问。

public void getMessage(Long id) {
	// 尝试在缓存中获取数据
	Message message = null;
	if (jedis.exists(id.toString())) {
		message = JSONObject.parseObject(jedis.get(id.toString()), Message.class);
		return;
	}
	// 如果缓存中没有,等待持有锁
	synchronized (this) {
		// 再次尝试在缓存中获取数据
		if (jedis.exists(id.toString())) {
			message = JSONObject.parseObject(jedis.get(id.toString()), Message.class);
			return;
		}
		// 开始查询数据库
		try (SqlSession session = sqlSessionFactory.openSession();) {
			MessageService messageService = session.getMapper(MessageService.class);
			message = messageService.getMessageById(id);
			jedis.set(id.toString(), JSONObject.toJSONString(message));
		}
	}
}

缓存雪崩

多个缓存数据在同一时间段内由于服务器故障等原因同时失效,称作缓存雪崩。解决方法:通过集群提高服务的可用性,避免单机故障引起的服务不可用。

解决方案

  • 主从复制
  • 哨兵模式
  • Cluster模式

缓存与数据库一致性

数据不一致问题出现的场景:

  • 当数据需要更新,无论先更新数据库或是缓存,如果中途发生意外,都有可能导致数据不一致。
  • 两个线程同时进行数据更新,由于错误的执行时序,数据库数据与缓存数据出现不一致问题。

解决方案一:延时双删

先删除缓存数据,再更新数据库,为了避免缓存在操作期间被更新,等待一段时间后再次删除缓存。这样便可以很大程度解决一致性问题,但仍有出现问题的可能性。

public void updateMessage(Message message) throws InterruptedException {
	// 删除缓存数据
	jedis.del(message.getId().toString());
	// 更新数据库
	try (SqlSession session = sqlSessionFactory.openSession();) {
		MessageService messageService = session.getMapper(MessageService.class);
		messageService.updateMessage(message);
		session.commit();
	}
	// 延时再次删除缓存
	Thread.sleep(200);
	jedis.del(message.getId().toString());
}

解决方案二:串行化

使查询操作与更新操作以串行的方式执行,避免更新数据期间缓存被写入,从而解决一致性问题。

public synchronized void getMessage() {
	// do something
}

public synchronized void updateMessage(Message message) throws InterruptedException {
	// 删除缓存数据
	jedis.del(message.getId().toString());
	// 更新数据库
	try (SqlSession session = sqlSessionFactory.openSession();) {
		MessageService messageService = session.getMapper(MessageService.class);
		messageService.updateMessage(message);
		session.commit();
	}
}