53、如何保证缓存与数据库双写时的数据一致性?

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。

image

在缓存与数据库双写的情况下,保证数据一致性是一个常见的挑战。以下是一些常用的方法和技术来确保缓存和数据库之间的数据一致性:

  1. 使用事务
  • 通过数据库事务来确保写操作的原子性,可以有效保证数据一致性。
    示例(使用SQL事务):

      using (var connection = new SqlConnection(connectionString))
      {
      	connection.Open();
      	using (var transaction = connection.BeginTransaction())
      	{
      		try
      		{
      			// 写入数据库
      			var command = new SqlCommand("UPDATE Users SET Name = @Name WHERE Id = @Id", connection, transaction);
      			command.Parameters.AddWithValue("@Name", newName);
      			command.Parameters.AddWithValue("@Id", userId);
      			command.ExecuteNonQuery();
    
      			// 写入缓存
      			db.StringSet($"user:{userId}:name", newName);
    
      			// 提交事务
      			transaction.Commit();
      		}
      		catch (Exception ex)
      		{
      			// 回滚事务
      			transaction.Rollback();
      			throw ex;
      		}
      	}
      }
    
  1. 缓存失效机制
    在写入数据库时,立即失效相应的缓存项,确保下次读取时从数据库获取最新数据。
    示例(缓存失效):

     using (var connection = new SqlConnection(connectionString))
     {
     	connection.Open();
     	using (var transaction = connection.BeginTransaction())
     	{
     		try
     		{
     			// 写入数据库
     			var command = new SqlCommand("UPDATE Users SET Name = @Name WHERE Id = @Id", connection, transaction);
     			command.Parameters.AddWithValue("@Name", newName);
     			command.Parameters.AddWithValue("@Id", userId);
     			command.ExecuteNonQuery();
    
     			// 提交事务
     			transaction.Commit();
    
     			// 失效缓存
     			db.KeyDelete($"user:{userId}:name");
     		}
     		catch (Exception ex)
     		{
     			// 回滚事务
     			transaction.Rollback();
     			throw ex;
     		}
     	}
     }
    
  2. 发布/订阅模式
    通过发布/订阅模式,当数据库写操作发生时,通知缓存系统失效或更新相应的缓存项。
    示例(发布/订阅):

     // 写入数据库
     using (var connection = new SqlConnection(connectionString))
     {
     	connection.Open();
     	using (var transaction = connection.BeginTransaction())
     	{
     		try
     		{
     			var command = new SqlCommand("UPDATE Users SET Name = @Name WHERE Id = @Id", connection, transaction);
     			command.Parameters.AddWithValue("@Name", newName);
     			command.Parameters.AddWithValue("@Id", userId);
     			command.ExecuteNonQuery();
    
     			// 提交事务
     			transaction.Commit();
    
     			// 发布消息到Redis频道
     			redis.Publish("cache:invalidate", $"user:{userId}:name");
     		}
     		catch (Exception ex)
     		{
     			// 回滚事务
     			transaction.Rollback();
     			throw ex;
     		}
     	}
     }
    
     // 订阅消息并失效缓存
     redis.Subscribe("cache:invalidate", (channel, message) =>
     {
     	db.KeyDelete(message.ToString());
     });
    
  3. 分布式锁
    使用分布式锁来确保在同一时间只有一个进程可以进行写操作,避免并发写操作导致的数据不一致。
    示例(使用Redis分布式锁):

     using StackExchange.Redis;
    
     var redis = ConnectionMultiplexer.Connect("localhost:6379");
     var db = redis.GetDatabase();
     var lockKey = $"lock:user:{userId}";
    
     using (var redisLock = db.LockTake(lockKey, TimeSpan.FromSeconds(10)))
     {
     	if (redisLock)
     	{
     		try
     		{
     			// 写入数据库
     			using (var connection = new SqlConnection(connectionString))
     			{
     				connection.Open();
     				var command = new SqlCommand("UPDATE Users SET Name = @Name WHERE Id = @Id", connection);
     				command.Parameters.AddWithValue("@Name", newName);
     				command.Parameters.AddWithValue("@Id", userId);
     				command.ExecuteNonQuery();
     			}
    
     			// 写入缓存
     			db.StringSet($"user:{userId}:name", newName);
     		}
     		catch (Exception ex)
     		{
     			// 处理异常
     			throw ex;
     		}
     		finally
     		{
     			// 释放锁
     			db.LockRelease(lockKey);
     		}
     	}
     }
    
  4. 幂等写操作
    确保写操作是幂等的,即多次执行相同的操作不会改变结果。这样可以在写操作失败后重试,而不会导致数据不一致。
    示例(幂等写操作):

     using (var connection = new SqlConnection(connectionString))
     {
     	connection.Open();
     	using (var transaction = connection.BeginTransaction())
     	{
     		try
     		{
     			// 插入或更新数据库
     			var command = new SqlCommand("MERGE Users USING (SELECT @Id AS Id, @Name AS Name) AS Source(Id, Name) ON Users.Id = Source.Id WHEN MATCHED THEN UPDATE SET Users.Name = Source.Name WHEN NOT MATCHED THEN INSERT (Id, Name) VALUES (Source.Id, Source.Name)", connection, transaction);
     			command.Parameters.AddWithValue("@Name", newName);
     			command.Parameters.AddWithValue("@Id", userId);
     			command.ExecuteNonQuery();
    
     			// 写入缓存
     			db.StringSet($"user:{userId}:name", newName);
    
     			// 提交事务
     			transaction.Commit();
     		}
     		catch (Exception ex)
     		{
     			// 回滚事务
     			transaction.Rollback();
     			throw ex;
     		}
     	}
     }
    
  5. 使用消息队列
    通过消息队列(如RabbitMQ、Kafka)来异步处理写操作,确保缓存和数据库的写操作顺序一致。
    示例(使用RabbitMQ):

     using RabbitMQ.Client;
     using StackExchange.Redis;
    
     var factory = new ConnectionFactory() { HostName = "localhost" };
     using (var connection = factory.CreateConnection())
     {
     	using (var channel = connection.CreateModel())
     	{
     		channel.QueueDeclare(queue: "user_updates", durable: false, exclusive: false, autoDelete: false, arguments: null);
    
     		// 发送消息到队列
     		var body = Encoding.UTF8.GetBytes($"{userId}:{newName}");
     		channel.BasicPublish(exchange: "", routingKey: "user_updates", basicProperties: null, body: body);
     	}
     }
    
     // 消费者监听队列并更新数据库和缓存
     var consumerConnection = factory.CreateConnection();
     using (var consumerChannel = consumerConnection.CreateModel())
     {
     	consumerChannel.QueueDeclare(queue: "user_updates", durable: false, exclusive: false, autoDelete: false, arguments: null);
    
     	var consumer = new EventingBasicConsumer(consumerChannel);
     	consumer.Received += (model, ea) =>
     	{
     		var body = ea.Body.ToArray();
     		var message = Encoding.UTF8.GetString(body);
     		var parts = message.Split(':');
     		var userId = parts[0];
     		var newName = parts[1];
    
     		using (var connection = new SqlConnection(connectionString))
     		{
     			connection.Open();
     			using (var transaction = connection.BeginTransaction())
     			{
     				try
     				{
     					// 更新数据库
     					var command = new SqlCommand("UPDATE Users SET Name = @Name WHERE Id = @Id", connection, transaction);
     					command.Parameters.AddWithValue("@Name", newName);
     					command.Parameters.AddWithValue("@Id", userId);
     					command.ExecuteNonQuery();
    
     					// 写入缓存
     					db.StringSet($"user:{userId}:name", newName);
    
     					// 提交事务
     					transaction.Commit();
     				}
     				catch (Exception ex)
     				{
     					// 回滚事务
     					transaction.Rollback();
     					throw ex;
     				}
     			}
     		}
     	};
    
     	consumerChannel.BasicConsume(queue: "user_updates", autoAck: true, consumer: consumer);
     }
    
  6. 数据版本控制
    通过版本号或时间戳来管理数据的版本,确保缓存中的数据与数据库中的数据一致。
    示例(数据版本控制):

     using (var connection = new SqlConnection(connectionString))
     {
     	connection.Open();
     	using (var transaction = connection.BeginTransaction())
     	{
     		try
     		{
     			// 写入数据库并获取新版本号
     			var command = new SqlCommand("UPDATE Users SET Name = @Name, Version = Version + 1 WHERE Id = @Id; SELECT Version FROM Users WHERE Id = @Id", connection, transaction);
     			command.Parameters.AddWithValue("@Name", newName);
     			command.Parameters.AddWithValue("@Id", userId);
     			var version = (int)command.ExecuteScalar();
    
     			// 写入缓存,包含版本号
     			db.StringSet($"user:{userId}", $"{newName}:{version}");
    
     			// 提交事务
     			transaction.Commit();
     		}
     		catch (Exception ex)
     		{
     			// 回滚事务
     			transaction.Rollback();
     			throw ex;
     		}
     	}
     }
    
     // 读取缓存时检查版本号
     var cachedValue = db.StringGet($"user:{userId}");
     if (!cachedValue.IsNullOrEmpty())
     {
     	var parts = cachedValue.ToString().Split(':');
     	var cachedName = parts[0];
     	var cachedVersion = int.Parse(parts[1]);
    
     	using (var connection = new SqlConnection(connectionString))
     	{
     		connection.Open();
     		var command = new SqlCommand("SELECT Version FROM Users WHERE Id = @Id", connection);
     		command.Parameters.AddWithValue("@Id", userId);
     		var dbVersion = (int)command.ExecuteScalar();
    
     		if (cachedVersion == dbVersion)
     		{
     			// 版本一致,使用缓存数据
     			Console.WriteLine($"Using cached data: {cachedName}");
     		}
     		else
     		{
     			// 版本不一致,从数据库获取最新数据
     			var latestNameCommand = new SqlCommand("SELECT Name FROM Users WHERE Id = @Id", connection);
     			latestNameCommand.Parameters.AddWithValue("@Id", userId);
     			var latestName = (string)latestNameCommand.ExecuteScalar();
     			Console.WriteLine($"Using latest data from database: {latestName}");
    
     			// 更新缓存
     			db.StringSet($"user:{userId}", $"{latestName}:{dbVersion}");
     		}
     	}
     }
     else
     {
     	// 缓存中没有数据,从数据库获取
     	using (var connection = new SqlConnection(connectionString))
     	{
     		connection.Open();
     		var command = new SqlCommand("SELECT Name, Version FROM Users WHERE Id = @Id", connection);
     		command.Parameters.AddWithValue("@Id", userId);
     		using (var reader = command.ExecuteReader())
     		{
     			if (reader.Read())
     			{
     				var latestName = reader.GetString(0);
     				var version = reader.GetInt32(1);
     				Console.WriteLine($"Using latest data from database: {latestName}");
    
     				// 更新缓存
     				db.StringSet($"user:{userId}", $"{latestName}:{version}");
     			}
     		}
     	}
     }
    

总结

通过上述方法,可以在缓存与数据库双写的情况下保证数据一致性。关键在于确保写操作的原子性、使用合适的同步机制、以及通过版本控制来管理数据的不同版本。这些方法可以帮助避免数据不一致的问题,确保系统的稳定性和可靠性。

posted @   似梦亦非梦  阅读(28)  评论(0编辑  收藏  举报
编辑推荐:
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
点击右上角即可分享
微信分享提示