• 企业级自定义表单引擎解决方案(十)缓存设计2


      新年伊始,万物皆生机,然冠未去,美帝相向,于华夏之子,吾辈当自强。

      这篇文章接上一篇文章,主要介绍缓存的代码实现

    后端本地缓存

      之前介绍的将自定义表单数据全部存储到应用程序内存中,任何自定义表单数据更新之后,都刷新内存缓存,分布式部署涉及到缓存同步刷新问题。

    • 全局本地缓存容器设计
    1. 用线程安全的字典ConcurrentDictionary<string, object> CacheDict,存储每一个数据对象集合,比如视图集合、表单集合等,每一次数据变更都清除具体的一个字典项数据
    2. 绝大多数时间都是读取缓存内容,因此这里上的读写锁,读写每一项缓存时,都上自己的读锁,锁的集合存储在ConcurrentDictionary<string, ReaderWriterLock> CacheReaderWriterLockDict变量中,Key与CacheDict的Key相同。
    3. 当检测到缓存通知服务断开时,会将本地所有缓存清空,直接读取原始数据库,用bool IsEnabledLocalCache变量控制。
    4. 当读取缓存时,发现本地缓存没有数据,则调用具体加载数据委托方法,本地没有数据读取时,需要加锁,防止缓存穿透。

    具体代码如下:

    /// <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();
                });
            }
        }
    

    缓存变更处理

    1. 主要分为缓存变更通知与接收缓存变更处理,缓存变更只需要通知哪一个Key过期即可。
    2. 接收缓存变更处理比较简单,接收到缓存变更之后,将内存容器中对应的字典项删除即可。
    3. 缓存通知定义为接口,如果是单应用部署,直接调用删除本地缓存服务即可,如果是分布式部署,也会调用删除本地缓存数据,通知发送分布式通知到其他自定义表单应用服务器,其他自定义表单应用服务器接收到缓存变更通知时,删除本地缓存数据。
    • 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

  • 相关阅读:
    (原创)如何利用UDP协议封装一个数据包
    <acarousel> 轮播图片无法触屏滑动
    Groovy开发语言
    关于ViewStub标签
    关键字transient和Volatile
    Android中Activity启动模式
    Android 开源项目分类汇总
    AtomicInteger的并发处理
    Cygwin: died waiting for dll loading (转载)
    Android知识点(C2DM)
  • 原文地址:https://www.cnblogs.com/spritekuang/p/15876299.html
Copyright © 2020-2023  润新知