企业级自定义表单引擎解决方案(十)--缓存设计2
新年伊始,万物皆生机,然冠未去,美帝相向,于华夏之子,吾辈当自强。
这篇文章接上一篇文章,主要介绍缓存的代码实现
后端本地缓存
之前介绍的将自定义表单数据全部存储到应用程序内存中,任何自定义表单数据更新之后,都刷新内存缓存,分布式部署涉及到缓存同步刷新问题。
- 全局本地缓存容器设计
- 用线程安全的字典ConcurrentDictionary<string, object> CacheDict,存储每一个数据对象集合,比如视图集合、表单集合等,每一次数据变更都清除具体的一个字典项数据
- 绝大多数时间都是读取缓存内容,因此这里上的读写锁,读写每一项缓存时,都上自己的读锁,锁的集合存储在ConcurrentDictionary<string, ReaderWriterLock> CacheReaderWriterLockDict变量中,Key与CacheDict的Key相同。
- 当检测到缓存通知服务断开时,会将本地所有缓存清空,直接读取原始数据库,用bool IsEnabledLocalCache变量控制。
- 当读取缓存时,发现本地缓存没有数据,则调用具体加载数据委托方法,本地没有数据读取时,需要加锁,防止缓存穿透。
具体代码如下:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | /// <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代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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代码
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 | /// <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组件实现,代码没有太多的逻辑,阅读代码即可。
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | /// <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())); } } } } } |
- 具体缓存代码实现举例(以表单为例)
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 | 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实现,前端代码暂时没开源,阅读一下即可
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | 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
分类:
企业自定义表单引擎解决方案
posted on 2022-02-09 18:50 spritekuang 阅读(834) 评论(0) 编辑 收藏 举报
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体