新年伊始,萬物皆生機,然冠未去,美帝相向,於華夏之子,吾輩當自強。
這篇文章接上一篇文章,主要介紹緩存的代碼實現
后端本地緩存
之前介紹的將自定義表單數據全部存儲到應用程序內存中,任何自定義表單數據更新之后,都刷新內存緩存,分布式部署涉及到緩存同步刷新問題。
- 全局本地緩存容器設計
- 用線程安全的字典ConcurrentDictionary<string, object> CacheDict,存儲每一個數據對象集合,比如視圖集合、表單集合等,每一次數據變更都清除具體的一個字典項數據
- 絕大多數時間都是讀取緩存內容,因此這里上的讀寫鎖,讀寫每一項緩存時,都上自己的讀鎖,鎖的集合存儲在ConcurrentDictionary<string, ReaderWriterLock> CacheReaderWriterLockDict變量中,Key與CacheDict的Key相同。
- 當檢測到緩存通知服務斷開時,會將本地所有緩存清空,直接讀取原始數據庫,用bool IsEnabledLocalCache變量控制。
- 當讀取緩存時,發現本地緩存沒有數據,則調用具體加載數據委托方法,本地沒有數據讀取時,需要加鎖,防止緩存穿透。
具體代碼如下:
/// <summary>
/// 本地緩存容器
/// </summary>
public class LocalCacheContainer
{
private static ConcurrentDictionary<string, object> CacheDict;
private static ConcurrentDictionary<string, ReaderWriterLock> CacheReaderWriterLockDict;
static LocalCacheContainer()
{
CacheDict = new ConcurrentDictionary<string, object>();
CacheReaderWriterLockDict = new ConcurrentDictionary<string, ReaderWriterLock>();
}
public static bool IsEnabledLocalCache { get; private set; } = true;
/// <summary>
/// 緩存通知斷開時調用
/// </summary>
/// <param name="isEnabled">是否啟用緩存</param>
internal static void SetLocalCacheIsEnabled(bool isEnabled)
{
IsEnabledLocalCache = isEnabled;
if(!isEnabled)
{
ClearAllCache();
}
}
public static object Get(string key, Func<string, object> factory)
{
var readerWriterLock = GetReadWriteLock(key);
readerWriterLock.AcquireReaderLock(5000);
try
{
//return CacheDict.GetOrAdd(key, factory); // 緩存穿透?
if (CacheDict.ContainsKey(key))
{
return CacheDict.GetOrAdd(key, factory);
}
else
{
lock (string.Intern(key))
{
return CacheDict.GetOrAdd(key, factory);
}
}
}
finally
{
readerWriterLock.ReleaseReaderLock();
}
}
internal static void ClearCache(string key)
{
var readerWriterLock = GetReadWriteLock(key);
readerWriterLock.AcquireWriterLock(5000);
try
{
object objRemove;
CacheDict.TryRemove(key, out objRemove);
}
finally
{
readerWriterLock.ReleaseReaderLock();
}
}
// 清楚所有緩存信息
private static void ClearAllCache()
{
CacheDict.Clear();
CacheReaderWriterLockDict.Clear();
}
private static ReaderWriterLock GetReadWriteLock(string key)
{
return CacheReaderWriterLockDict.GetOrAdd(key, k =>
{
return new ReaderWriterLock();
});
}
}
緩存變更處理
- 主要分為緩存變更通知與接收緩存變更處理,緩存變更只需要通知哪一個Key過期即可。
- 接收緩存變更處理比較簡單,接收到緩存變更之后,將內存容器中對應的字典項刪除即可。
- 緩存通知定義為接口,如果是單應用部署,直接調用刪除本地緩存服務即可,如果是分布式部署,也會調用刪除本地緩存數據,通知發送分布式通知到其他自定義表單應用服務器,其他自定義表單應用服務器接收到緩存變更通知時,刪除本地緩存數據。
- ReceiveCacheNotice代碼
public static class ReceiveCacheNotice
{
public static void ReceiveClearCache(string key)
{
LocalCacheContainer.ClearCache(key);
}
public static void ReceiveClearCaches(List<string> keys)
{
foreach(var key in keys)
{
LocalCacheContainer.ClearCache(key);
}
}
public static void SetLocalCacheIsEnabled(bool isEnabled)
{
LocalCacheContainer.SetLocalCacheIsEnabled(isEnabled);
}
}
- ICacheSendNotice及本地通知LocalCacheSendNotice代碼
/// <summary>
/// 設計時實體變更通知緩存
/// </summary>
public interface ICacheSendNotice
{
/// <summary>
/// 發送緩存變更
/// </summary>
/// <param name="key">緩存Key</param>
void SendClearCache(string key);
/// <summary>
/// 發送緩存多個變更
/// </summary>
/// <param name="key">緩存Key集合</param>
void SendClearCaches(List<string> keys);
}
/// <summary>
/// 本地緩存容器通知服務
/// </summary>
public class LocalCacheSendNotice : ICacheSendNotice
{
public void SendClearCache(string key)
{
ReceiveCacheNotice.ReceiveClearCache(key);
}
public void SendClearCaches(List<string> keys)
{
ReceiveCacheNotice.ReceiveClearCaches(keys);
}
}
- 分布式緩存發布訂閱Redis實現,主要是用StackExchange.Redis組件實現,代碼沒有太多的邏輯,閱讀代碼即可。
/// <summary>
/// Redis緩存容器通知服務
/// </summary>
public class RedisCacheSendNotice : ICacheSendNotice
{
private readonly SpriteConfig _callHttpConfig;
private readonly IDistributedCache _distributedCache;
private readonly ISubscriber _subscriber;
public RedisCacheSendNotice(IDistributedCache distributedCache, IOptions<SpriteConfig> callHttpConfig)
{
_distributedCache = distributedCache;
_callHttpConfig = callHttpConfig.Value;
var spriteRedisCache = _distributedCache as SpriteRedisCache;
spriteRedisCache.RedisDatabase.Multiplexer.ConnectionFailed += Multiplexer_ConnectionFailed;
spriteRedisCache.RedisDatabase.Multiplexer.ConnectionRestored += Multiplexer_ConnectionRestored;
_subscriber = spriteRedisCache.RedisDatabase.Multiplexer.GetSubscriber();
if (_callHttpConfig.RemoteReceivePreKey != null)
{
foreach (var remoteReceivePreKey in _callHttpConfig.RemoteReceivePreKey)
{
_subscriber.Subscribe(remoteReceivePreKey, (channel, message) =>
{
ReceiveCacheNotice.ReceiveClearCache(message);
});
_subscriber.Subscribe($"{remoteReceivePreKey}s", (channel, message) =>
{
List<string> keys = JsonConvert.DeserializeObject<List<string>>(message);
ReceiveCacheNotice.ReceiveClearCaches(keys);
});
}
}
}
private void Multiplexer_ConnectionRestored(object sender, StackExchange.Redis.ConnectionFailedEventArgs e)
{
ReceiveCacheNotice.SetLocalCacheIsEnabled(true);
}
private void Multiplexer_ConnectionFailed(object sender, StackExchange.Redis.ConnectionFailedEventArgs e)
{
ReceiveCacheNotice.SetLocalCacheIsEnabled(false);
}
public void SendClearCache(string key)
{
ReceiveCacheNotice.ReceiveClearCache(key);
if (_callHttpConfig.RemoteNoticePreKey != null)
{
if (_callHttpConfig.RemoteNoticePreKey.Any(r => key.StartsWith($"{r}-")))
{
_subscriber.Publish(key.Split('-')[0], key);
}
}
}
public void SendClearCaches(List<string> keys)
{
ReceiveCacheNotice.ReceiveClearCaches(keys);
if (_callHttpConfig.RemoteNoticePreKey != null)
{
var groupKeyLists = keys.GroupBy(r => r.Split('-')[0]);
foreach (var groupKeyList in groupKeyLists)
{
if (_callHttpConfig.RemoteNoticePreKey.Any(r => groupKeyList.Key == r))
{
_subscriber.Publish($"{groupKeyList.Key}s", JsonConvert.SerializeObject(groupKeyList.ToList()));
}
}
}
}
}
- 具體緩存代碼實現舉例(以表單為例)
public class SpriteFormLocalCache : LocalCache<SpriteFormVueDto>
{
public override string CacheKey => CommonConsts.SpriteFormCacheKey;
public override Dictionary<Guid, SpriteFormVueDto> GetAllDict(string applicationCode)
{
if (!LocalCacheContainer.IsEnabledLocalCache) // 如果緩存通知服務不可以,直接讀取數據庫
{
return _serviceProvider.DoDapperService(DefaultDbConfig, (unitOfWork) =>
{
return GetSpriteFormVueDtos(applicationCode, unitOfWork);
});
}
else
{
// 讀取本地緩存內容,如果本地緩存沒有數據,讀取數據庫數據,並寫入本地緩存容器
return (Dictionary<Guid, SpriteFormVueDto>)LocalCacheContainer.Get($"{CommonConsts.SpriteFormCachePreKey}-{applicationCode}_{CacheKey}", key =>
{
return _serviceProvider.DoDapperService(DefaultDbConfig, (unitOfWork) =>
{
return GetSpriteFormVueDtos(applicationCode, unitOfWork);
});
});
}
}
......
}
- 前端緩存主要是用IndexDb實現,前端代碼暫時沒開源,閱讀一下即可
import Dexie from 'dexie'
import { SpriteRumtimeApi } from '@/sprite/api/spriteform'
const db = new Dexie('formDb')
db.version(1).stores({
form: `id`
})
db.version(1).stores({
view: `id`
})
db.version(1).stores({
frameworkCache: `id`
})
db.version(1).stores({
dict: `id`
})
window.spriteDb = db
db.menuFormRelationInfo = {}
const createMenuFormRelations = function (routeName, applicationCode, relationInfos) {
if (!db.menuFormRelationInfo.hasOwnProperty(routeName)) {
db.menuFormRelationInfo[routeName] = {}
db.menuFormRelationInfo[routeName].applicationCode = applicationCode
db.menuFormRelationInfo[routeName].relationInfos = relationInfos
} else {
relationInfos.forEach(relationInfo => {
if (!db.menuFormRelationInfo[routeName].relationInfos.find(r => r.relationType === relationInfo.relationType && r.id === relationInfo.id && r.version === relationInfo.version)) {
db.menuFormRelationInfo[routeName].relationInfos.push(relationInfo)
}
});
}
}
/**
* 遞歸獲取表單或視圖關聯表單視圖版本信息
* @param {guid} objId 表單或視圖Id
* @param {int} relationType 1=表單,2=視圖
* @param {obj} relationInfos 表單和視圖版本信息
*/
const findRelationConfigs = async function (objId, relationType, relationInfos) {
if (!relationInfos) {
relationInfos = []
}
console.log(relationType)
var findData = relationType === 1 ? await db.form.get(objId) : await db.view.get(objId)
if (findData && relationInfos.findIndex(r => r.id === findData.id) < 0) {
relationInfos.push({ relationType: relationType, id: findData.id, version: findData.version })
}
if (findData && findData.relationInfos && findData.relationInfos.length > 0) {
for (var i = 0; i < findData.relationInfos.length; i++) {
await findRelationConfigs(findData.relationInfos[i].id, findData.relationInfos[i].relationType, relationInfos)
}
}
console.log('relationInfos')
console.log(relationInfos)
return relationInfos
}
db.getFormData = async function (routeName, formId, fromMenu, applicationCode) {
var formData = await db.form.get(formId)
var dictFrameworkCache = await db.frameworkCache.get('dict')
console.log("getFormData")
if (!formData) {
var resultData = await SpriteRumtimeApi.simpleform({ id: formId, applicationCode: applicationCode })
var menuFormrelationInfos = []
if (resultData && resultData) {
for (var i = 0; i < resultData.formDatas.length; i++) {
await db.form.put(resultData.formDatas[i])
menuFormrelationInfos.push({relationType: 1, id: resultData.formDatas[i].id, version: resultData.formDatas[i].version})
}
for (var j = 0; j < resultData.viewDatas.length; j++) {
await db.view.put(resultData.viewDatas[j])
menuFormrelationInfos.push({relationType: 2, id: resultData.viewDatas[j].id, version: resultData.viewDatas[j].version})
}
}
if (resultData && resultData.dictVersion && resultData.dicts) {
await db.frameworkCache.put({ id: 'dict', version: resultData.dictVersion })
await db.dict.clear()
await db.dict.bulkAdd(resultData.dicts)
}
createMenuFormRelations(routeName, applicationCode, menuFormrelationInfos)
formData = await db.form.get(formId)
} else { // 從indexdb找到數據,如果從菜單進入,需要調用接口,判斷版本號信息
if (fromMenu) {
delete db.menuFormRelationInfo[routeName]
var relationInfos = await findRelationConfigs(formId, 1, [])
var relationParams = { applicationCode: applicationCode, formId: formId, relationInfos: relationInfos, dictVersion: dictFrameworkCache?.version }
var checkResult = await SpriteRumtimeApi.checkversions(relationParams)
if ((checkResult && checkResult.formDatas && checkResult.formDatas.length > 0) || (checkResult && checkResult.viewDatas && checkResult.viewDatas.length > 0)) {
relationInfos = []
}
if (checkResult && checkResult.formDatas && checkResult.formDatas.length > 0) {
for (var i2 = 0; i2 < checkResult.formDatas.length; i2++) {
await db.form.put(checkResult.formDatas[i2])
relationInfos.push({relationType: 1, id: checkResult.formDatas[i2].id, version: checkResult.formDatas[i2].version})
}
}
if (checkResult && checkResult.viewDatas && checkResult.viewDatas.length > 0) {
for (var j2 = 0; j2 < checkResult.viewDatas.length; j2++) {
await db.view.put(checkResult.viewDatas[j2])
relationInfos.push({relationType: 2, id: checkResult.viewDatas[j2].id, version: checkResult.viewDatas[j2].version})
}
}
if (checkResult && checkResult.dictVersion && checkResult.dicts) {
await db.frameworkCache.put({ id: 'dict', version: checkResult.dictVersion })
await db.dict.clear()
await db.dict.bulkAdd(checkResult.dicts)
}
createMenuFormRelations(routeName, applicationCode, relationInfos)
formData = await db.form.get(formId)
}
}
return formData
}
開源地址:https://gitee.com/kuangqifu/sprite
體驗地址:http://47.108.141.193:8031(首次加載可能有點慢,用的阿里雲最差的服務器)
自定義表單文章地址:https://www.cnblogs.com/spritekuang/
流程引擎文章地址:https://www.cnblogs.com/spritekuang/category/834975.html(采用WWF開發,已過時,已改用Elsa實現,https://www.cnblogs.com/spritekuang/p/14970992.html )
Github地址:https://github.com/kuangqifu/CK.Sprite.Job
