数据一致性问题
管理员修改或删除了MySQL中的数据而没有及时更新Redis中的数据,用户访问这条数据时可能会从Redis中获取到旧的数据,导致数据不一致。比如,管理员将MySQL中的id=1的数据从"旧"修改为"新",但是Redis缓存中的数据仍然是"旧",当用户访问这条数据时,经过上述缓存业务逻辑,会从Redis中获取到"旧"的数据,而MySQL中的数据已经更新为"新",这样就会导致Redis中的数据和MySQL中的数据不一致。
双写模式
失效模式
双删模式
双删模式是指在进行更新或删除操作时,先删除缓存中的数据,再进行数据库操作,最后再次删除缓存中的数据。这种方式可以保证数据一致性,因为如果在进行数据库操作时出现异常,数据不会被缓存,下次访问时会再次从数据库中获取最新数据并进行缓存。如果先进行数据库操作再删除缓存,如果在数据库操作时出现异常,那么已经删除的缓存将导致下次访问时无法获取最新数据,导致数据不一致。
双删案例演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
private static final String KEY_PREFIX = "INDEX:CATES:";
@Autowired private StringRedisTemplate redisTemplate;
@Autowired private RabbitTemplate rabbitTemplate;
@Override @Transactional public void update(CategoryEntity category) {
String key = KEY_PREFIX + category.getParentId();
redisTemplate.delete(key);
updateById(category);
rabbitTemplate.convertAndSend("PMS_CATEGORY_EXCHANGE", "category.update", key);
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| rabbitmq: host: 192.168.0.101 port: 5672 virtual-host: /admin username: admin password: admin listener: type: simple simple: prefetch: 1 concurrency: 8 acknowledge-mode: manual publisher-confirm-type: correlated publisher-returns: true
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| @Component public class CategoryListener {
@Autowired private StringRedisTemplate redisTemplate;
@RabbitListener(bindings = @QueueBinding( // 声明绑定关系 // 绑定的 队列, 将下方声明的交换机绑定给此队列 value = @Queue(value = "PMS_CATEGORY_QUEUE", durable = "true", // 使用 durable 指定是否需要持久化 默认是 true // 队列存在的情况下 如果声明一个属性与之前队列不一样 rabbitmq 就会报声明错误, ignoreDeclarationExceptions 可以使用忽略声明异常进行忽略(可以忽略声明异常使用既有的) ignoreDeclarationExceptions = "true"), // 默认队列不需要设置 // 需要与生产者中的交换机一致, type 交换机类型 exchange = @Exchange(value = "PMS_CATEGORY_EXCHANGE", ignoreDeclarationExceptions = "ture", // 通常在 交换机中设置忽略声明异常, 可以避免重复声明 // 通配模型 type = ExchangeTypes.TOPIC), // 绑定的 交换机 // key = {"category.update, category.delete"} // rk, 可以绑定多个 key = {"category.*"} // rk, 可以绑定多个 )) public void syncData(String key, Channel channel, Message message) throws IOException {
long startTime = System.currentTimeMillis(); System.err.println("异步删除缓存开始: " + startTime + "ms");
if (StringUtils.isBlank(key)) { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; }
try { redisTemplate.delete(key);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { e.printStackTrace();
if (message.getMessageProperties().getRedelivered()) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } else { channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); } }
long endTime = System.currentTimeMillis(); System.err.println("异步删除缓存结束: " + startTime + "ms"); System.err.println("异步删除缓存结束! 总耗时: " + (endTime - startTime) + "ms"); }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Configuration public class RabbitConfig {
@Autowired private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() { rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> { if (ack) { System.out.println("消息已到达交换机"); } else { System.err.println("消息没有达到交换机: 原因 " + cause); } });
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> System.err.println("消息没有到达队列: " + " 交换机 " + exchange + " 路由键 " + routingKey + " 消息内容 " + replyText + " 状态码 " + replyCode + " 消息内容 " + new String(message.getBody())) ); } }
|