1. CAP 原则
CAP 原则也称为布鲁尔定理,由 Eric Brewer 在 2000 年提出,描述了分布式系统中的三个核心属性:一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)。CAP 原则指出在分布式系统中,无法同时保证这三个属性,最多只能满足其中的两个。
-
一致性(Consistency):系统对外表现为单个节点上的数据总是最新的,所有读请求都能获取到最近一次写入的数据。例如,像银行的交易系统,这种系统必须保持严格的一致性。
-
可用性(Availability):系统能够始终对请求做出响应,即使部分节点故障,系统依然可以继续服务。例如,像亚马逊和谷歌这样的电商和搜索引擎系统,即使部分服务器出现问题,依然能保证大部分用户的访问。
-
分区容错性(Partition Tolerance):当分布式系统的不同节点之间发生网络分区时,系统能够继续工作,而不发生崩溃或错误。例如,跨地域的分布式数据库系统需要在网络分区的情况下,仍保持系统的高可用性。
实际案例:
- 一致性优先:像银行系统、股票交易系统,这些系统需要确保每次查询的结果都是准确无误的,所以更注重数据一致性。
- 可用性优先:像电商、视频网站等,在这种场景下,哪怕数据可能不是最新的,也需要保证系统的响应速度和用户体验。
- 分区容错性优先:跨地域的社交网络、全球范围内的支付系统等,由于其涉及多个地理区域,网络延迟和网络分区问题普遍存在,系统需要具备分区容错性。
2. 实战准备
我们将创建一个 MySQL 数据库和 Redis 缓存,并设计两个操作:
- 更新数据:数据库和缓存都需要更新。
- 查询数据:优先从缓存中查询,如果缓存不存在,再从数据库中查询。
- 删除缓存:当缓存过期时或数据被更新时,需要删除缓存。
示例准备:
// 安装 MySql.Data 和 StackExchange.Redisusing MySql.Data.MySqlClient;
using StackExchange.Redis;
using System;
using System.Threading;class CacheWithDatabase
{private static MySqlConnection dbConnection;private static ConnectionMultiplexer redisConnection;private static IDatabase redisCache;static void Main(string[] args){// 初始化数据库连接string dbConnectionString = "Server=localhost;Database=testdb;Uid=root;Pwd=password;";dbConnection = new MySqlConnection(dbConnectionString);dbConnection.Open();// 初始化 Redis 连接redisConnection = ConnectionMultiplexer.Connect("localhost");redisCache = redisConnection.GetDatabase();// 模拟缓存与数据库的操作UpdateData("key1", "new value");string value = GetData("key1");Console.WriteLine("Retrieved value: " + value);// 删除缓存DeleteCache("key1");}// 更新数据方法static void UpdateData(string key, string newValue){// 更新数据库string query = "UPDATE test_table SET value = @newValue WHERE key = @key";using (MySqlCommand cmd = new MySqlCommand(query, dbConnection)){cmd.Parameters.AddWithValue("@newValue", newValue);cmd.Parameters.AddWithValue("@key", key);cmd.ExecuteNonQuery();}// 更新缓存redisCache.StringSet(key, newValue);Console.WriteLine("Updated cache with key: " + key);}// 查询数据方法static string GetData(string key){// 先查询缓存string cachedValue = redisCache.StringGet(key);if (cachedValue != null){Console.WriteLine("Cache hit: " + key);return cachedValue;}// 如果缓存没有命中,则查询数据库string query = "SELECT value FROM test_table WHERE key = @key";using (MySqlCommand cmd = new MySqlCommand(query, dbConnection)){cmd.Parameters.AddWithValue("@key", key);string dbValue = (string)cmd.ExecuteScalar();if (dbValue != null){redisCache.StringSet(key, dbValue); // 将数据缓存Console.WriteLine("Cache miss. Database hit: " + key);return dbValue;}return null;}}// 删除缓存方法static void DeleteCache(string key){redisCache.KeyDelete(key);Console.WriteLine("Cache deleted for key: " + key);}
}
3. 缓存更新策略分析
缓存更新策略在高并发场景下尤为重要,避免不一致性和性能瓶颈的挑战,常见的缓存更新策略有以下几种:
- 写回缓存:在写数据时同时更新缓存和数据库。
- 缓存失效:缓存与数据库更新保持异步,数据变更时仅删除缓存,让后续查询自行更新。
- 定时刷新:定期刷新缓存的数据。
4. 方案 1 - 先更新缓存,再更新数据库
在此方案中,首先更新缓存,再更新数据库,这种方式可以保证较高的系统性能,因为用户查询时可以快速获得缓存中的最新数据。
多线程示例:
static void UpdateDataWithPriority(string key, string newValue)
{Thread cacheThread = new Thread(() => {// 更新缓存redisCache.StringSet(key, newValue);Console.WriteLine("Cache updated with priority for key: " + key);});Thread dbThread = new Thread(() =>{// 更新数据库string query = "UPDATE test_table SET value = @newValue WHERE key = @key";using (MySqlCommand cmd = new MySqlCommand(query, dbConnection)){cmd.Parameters.AddWithValue("@newValue", newValue);cmd.Parameters.AddWithValue("@key", key);cmd.ExecuteNonQuery();}Console.WriteLine("Database updated for key: " + key);});cacheThread.Start();dbThread.Start();cacheThread.Join();dbThread.Join();
}
5. 方案 2 - 先更新数据库,再更新缓存
此策略的优点是保证数据持久化安全性,先将数据存入数据库,减少丢失数据的风险。
static void UpdateDataAfterDB(string key, string newValue)
{Thread dbThread = new Thread(() =>{// 更新数据库string query = "UPDATE test_table SET value = @newValue WHERE key = @key";using (MySqlCommand cmd = new MySqlCommand(query, dbConnection)){cmd.Parameters.AddWithValue("@newValue", newValue);cmd.Parameters.AddWithValue("@key", key);cmd.ExecuteNonQuery();}Console.WriteLine("Database updated for key: " + key);});Thread cacheThread = new Thread(() => {// 更新缓存redisCache.StringSet(key, newValue);Console.WriteLine("Cache updated after database update for key: " + key);});dbThread.Start();dbThread.Join(); // 确保数据库先更新cacheThread.Start();
}
6. 方案 3 - 先删除缓存,再更新数据库
static void UpdateAfterCacheDeletion(string key, string newValue)
{Thread cacheDeletionThread = new Thread(() => {// 删除缓存redisCache.KeyDelete(key);Console.WriteLine("Cache deleted for key: " + key);});Thread dbUpdateThread = new Thread(() =>{// 更新数据库string query = "UPDATE test_table SET value = @newValue WHERE key = @key";using (MySqlCommand cmd = new MySqlCommand(query, dbConnection)){cmd.Parameters.AddWithValue("@newValue", newValue);cmd.Parameters.AddWithValue("@key", key);cmd.ExecuteNonQuery();}Console.WriteLine("Database updated for key: " + key);});cacheDeletionThread.Start();dbUpdateThread.Start();cacheDeletionThread.Join();dbUpdateThread.Join();
}
7. 方案 4 - 先更新数据库,再删除缓存
static void UpdateAfterDBThenDeleteCache(string key, string newValue)
{Thread dbUpdateThread = new Thread(() =>{// 更新数据库string query = "UPDATE test_table SET value = @newValue WHERE key = @key";using (MySqlCommand cmd = new MySqlCommand(query, dbConnection)){cmd.Parameters.AddWithValue("@newValue", newValue);cmd.Parameters.AddWithValue("@key", key);cmd.ExecuteNonQuery();}Console.WriteLine("Database updated for key: " + key);});Thread cacheDeletionThread = new Thread(() => {// 删除缓存redisCache.KeyDelete(key);Console.WriteLine("Cache deleted for key: " + key);});dbUpdateThread.Start();dbUpdateThread.Join(); // 确保数据库更新后再删除缓存cacheDeletionThread.Start();
}
8. 最终方案
结合上述方案,在高并发场景下,我们倾向于采用先更新数据库,再删除缓存的方式。这样可以保证数据的一致性,避免缓存中的脏数据,同时提升系统性能。以下是方案的流程图。
流程图
上面是基于高并发分布式缓存更新策略的流程图。此方案遵循先更新数据库,再删除缓存的逻辑,并采用多线程处理。在高并发情况下,确保了数据库和缓存的一致性及系统的高可用性。