那些年我们一起追过的缓存写法(三)

本文详细解析了内存缓存设计的关键问题,包括内存与Redis的数据置换、内存容量限制、热点数据更新策略、良好的缓存过期删除策略等。通过引入LRU算法并实现O(1)复杂度的LRU实现,文章阐述了如何优化内存缓存的性能。此外,还讨论了过期删除策略,包括LRU-K、2Q、MQ算法的使用以及单个key的过期时间配置。最后,文章总结了内存缓存设计的最佳实践,并提供了一种结合惰性删除与定期清理的策略,以应对缓存污染问题。

那些年我们一起追过的缓存写法(三)

2015-02-08 08:45 by 蘑菇先生, 4214 阅读, 11 评论, 收藏编辑

     上篇介绍了多级缓存,本章详细介绍下内存缓存该如何设计。

阅读目录:

  1. 分析设计
  2. O(1)LRU实现
  3. 过期删除策略
  4. 总结

分析设计

假设有个项目有比较高的并发量,要用到多级缓存,如下:

在实际设计一个内存缓存前,需要考虑的问题:

1:内存与Redis的数据置换,尽可能在内存中提高数据命中率,减少下一级的压力。

2:内存容量的限制,需要控制缓存数量。

3:热点数据更新不同,需要可配置单个key过期时间。

4:良好的缓存过期删除策略

5:缓存数据结构的复杂度尽可能的低。

关于置换及命中率:采用LRU算法,因为它实现简单,缓存key命中率也很好。

                          LRU即是:把最近最少访问的数据给淘汰掉,经常被访问到即是热点数据。

关于LRU数据结构:因为key优先级提升和key淘汰,所以要顺序结构,网上大多实现都采用的这种链表结构。

                         即新数据插入到链表头部、被命中时的数据移动到头部,添加复杂度O(1),移动和获取复杂度O(N)。

有没复杂度更低的呢? 有Dictionary,复杂度为O(1),性能最好。 那如何保证缓存的优先级提升呢?

O(1)LRU实现

定义个LRUCache<TValue>类,构造参数maxKeySize 来控制缓存最大数量。

使用ConcurrentDictionary来作为我们的缓存容器,并能保证线程安全。

复制代码
 public class LRUCache<TValue> : IEnumerable<KeyValuePair<string, TValue>>
    {
        private long ageToDiscard = 0;  //淘汰的年龄起点
        private long currentAge = 0;        //当前缓存最新年龄
        private int maxSize = 0;          //缓存最大容量
        private readonly ConcurrentDictionary<string, TrackValue> cache;
        public LRUCache(int maxKeySize)
        {
            cache = new ConcurrentDictionary<string, TrackValue>();
            maxSize = maxKeySize;
        }
    }
复制代码

上面定义了 ageToDiscard、currentAge 这2个自增值参数,作用是标记缓存列表中各个key的新旧程度。

实现步骤如下:

每次添加key时,currentAge自增并将currentAge值分配给这个缓存值的age,currentAge一直自增。

复制代码
 public void Add(string key, TValue value)
        {
            Adjust(key);
            var result = new TrackValue(this, value);
            cache.AddOrUpdate(key, result, (k, o) => result);
        }
        public class TrackValue
        {
            public readonly TValue Value;
            public long Age;
            public TrackValue(LRUCache<TValue> lv, TValue tv)
            {
                Age = Interlocked.Increment(ref lv.currentAge);
                Value = tv;
            }
        }
复制代码

在添加时,如超过最大数量,检查字典里是否有ageToDiscard年龄的key,如没有循环自增检查,有则删除、添加成功。

其ageToDiscard+maxSize= currentAge ,这样设计就能在O(1)下保证可以淘汰旧数据,而不是使用链表移动。 

复制代码
  public void Adjust(string key)
        {
            while (cache.Count >= maxSize)
            {
                long ageToDelete = Interlocked.Increment(ref ageToDiscard);
                var toDiscard =
                      cache.FirstOrDefault(p => p.Value.Age == ageToDelete);
                if (toDiscard.Key == null)
                    continue;
                TrackValue old;
                cache.TryRemove(toDiscard.Key, out old);
            }
        }
复制代码

获取key的时候表示它又被人访问,将最新的currentAge赋值给它,增加它的年龄:

复制代码
  public TValue Get(string key)
        {
            TrackValue value=null;
            if (cache.TryGetValue(key, out value))
            {
                value.Age = Interlocked.Increment(ref currentAge);
            }
            return value.Value;
        }
复制代码

过期删除策略

大多数情况下,LRU算法对热点数据命中率是很高的。 但如果突然大量偶发性的数据访问,会让内存中存放大量冷数据,也即是缓存污染。

会引起LRU无法命中热点数据,导致缓存系统命中率急剧下降,也可以使用LRU-K、2Q、MQ等变种算法来提高命中率。

过期配置

通过设定最大过期时间来尽量避免冷数据常驻内存。

多数情况每个数据缓存的时间要求不一致的,所以需要再增加单个key的过期时间字段。

复制代码
 private TimeSpan maxTime;
 public LRUCache(int maxKeySize,TimeSpan maxExpireTime){}

  //TrackValue增加创建时间和过期时间
 public readonly DateTime CreateTime;
 public readonly TimeSpan ExpireTime;
复制代码

删除策略

关于key过期删除,最好的方式是使用定时删除,这样可以最快的释放被占用的内存,但很明显大量的定时器对CPU来说是非常不友好的。

所以需要采用惰性删除、在获取key的时检查是否过期,过期直接删除。

复制代码
public Tuple<TrackValue, bool> CheckExpire(string key)
        {
            TrackValue result;
            if (cache.TryGetValue(key, out result))
            {
                var age = DateTime.Now.Subtract(result.CreateTime);
                if (age >= maxTime || age >= result.ExpireTime)
                {
                    TrackValue old;
                    cache.TryRemove(key, out old);
                    return Tuple.Create(default(TrackValue), false);
                }
            }
            return Tuple.Create(result, true);
        }
复制代码

惰性删除虽然性能最好,但对于冷数据来说还是没解决缓存污染的问题,所以还需增加个定期清理和惰性删除配合使用。

比如单开个线程每5分钟去遍历检查key是否过期,这个时间策略是可配置的,如果缓存数量较多可分批遍历检查。

复制代码
public void Inspection()
        {
            foreach (var item in this)
            {
                CheckExpire(item.Key);
            }
        }
复制代码

惰性删除配合定期删除基本上能满足绝大多数要求了。

总结

本篇参考了redis、Orleans的相关实现。

如果继续完善下去就是内存数据库的雏形,类似redis,比如增加删除key的通知回调,支持更多的数据类型存储。

系列目录:

那些年我们一起追过的缓存写法(一)

那些年我们一起追过的缓存写法(二) 

那些年我们一起追过的缓存写法(三) 

同步定位与地图构建(SLAM)技术为移动机器人或自主载具在未知空间中的导航提供了核心支撑。借助该技术,机器人能够在探索过程中实时构建环境地图并确定自身位置。典型的SLAM流程涵盖传感器数据采集、数据处理、状态估计及地图生成等环节,其核心挑战在于有效处理定位与环境建模中的各类不确定性。 Matlab作为工程计算与数据可视化领域广泛应用的数学软件,具备丰富的内置函数与专用工具箱,尤其适用于算法开发与仿真验证。在SLAM研究方面,Matlab可用于模拟传感器输出、实现定位建图算法,并进行系统性能评估。其仿真环境能显著降低实验成本,加速算法开发与验证周期。 本次“SLAM-基于Matlab的同步定位与建图仿真实践项目”通过Matlab平台完整再现了SLAM的关键流程,包括数据采集、滤波估计、特征提取、数据关联与地图更新等核心模块。该项目不仅呈现了SLAM技术的实际应用场景,更为机器人导航与自主移动领域的研究人员提供了系统的实践参考。 项目涉及的核心技术要点主要包括:传感器模型(如激光雷达与视觉传感器)的建立与应用、特征匹配与数据关联方法、滤波器设计(如扩展卡尔曼滤波与粒子滤波)、图优化框架(如GTSAM与Ceres Solver)以及路径规划与避障策略。通过项目实践,参与者可深入掌握SLAM算法的实现原理,并提升相关算法的设计与调试能力。 该项目同时注重理论向工程实践的转化,为机器人技术领域的学习者提供了宝贵的实操经验。Matlab仿真环境将复杂的技术问题可视化与可操作化,显著降低了学习门槛,提升了学习效率与质量。 实践过程中,学习者将直面SLAM技术在实际应用中遇到的典型问题,包括传感器误差补偿、动态环境下的建图定位挑战以及计算资源优化等。这些问题的解决对推动SLAM技术的产业化应用具有重要价值。 SLAM技术在工业自动化、服务机器人、自动驾驶及无人机等领域的应用前景广阔。掌握该项技术不仅有助于提升个人专业能力,也为相关行业的技术发展提供了重要支撑。随着技术进步与应用场景的持续拓展,SLAM技术的重要性将日益凸显。 本实践项目作为综合性学习资源,为机器人技术领域的专业人员提供了深入研习SLAM技术的实践平台。通过Matlab这一高效工具,参与者能够直观理解SLAM的实现过程,掌握关键算法,并将理论知识系统应用于实际工程问题的解决之中。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
<think>我们有一个下拉列表(select元素)的ID是`certTypeList`,里面有多个option选项。 需求:当变量`personflag`等于1时,我们只显示值为6和9的选项,其他选项都隐藏(或者移除,但隐藏更便于恢复)。 实现思路:遍历所有option,如果option的值不是6也不是9,则将其隐藏(或移除)。如果是6或9,则显示(如果之前被隐藏过,可能需要显示,但因为可能是初始状态,所以也可以统一处理)。 注意:由于后续可能还需要恢复(比如personflag变化时),所以建议使用隐藏(hide)和显示(show)的方式,而不是移除(remove)。 但是题目要求基于`$('#certTypeList option').each(function(i, o)`代码来实现,所以我们在这个each循环中操作。 步骤: 1. 判断personflag是否等于1。 2. 如果是,则遍历每个option,检查其value属性。 3. 如果value是6或者9,就显示(使用show或保留显示),否则隐藏(hide)。 4. 如果不是personflag等于1的情况,我们可能还需要恢复(但题目没有要求,所以这里只处理personflag等于1的情况)。 但是,题目要求只处理personflag等于1的情况,所以我们可以这样写: 注意:由于我们可能之前隐藏了一些选项,所以在其他情况下(比如personflag不等于1)可能需要显示所有选项,但题目没有明确,所以这里只完成题目要求。 考虑到题目要求,我们只写personflag等于1的处理。 另外,如果personflag不等于1,我们不需要做任何操作(或者根据需求,可能要求显示所有选项,但题目没有说,所以这里不做)。 代码: ```javascript // 假设personflag是一个变量,已经存在 if (personflag == 1) { $('#certTypeList option').each(function(i, o) { // o是原生的option元素,我们可以获取其value var value = $(o).val(); // 或者用o.value,但为了统一用jQuery,这里用$(o).val() // 如果值是6或9,则显示,否则隐藏 if (value == '6' || value == '9') { $(o).show(); // 显示 } else { $(o).hide(); // 隐藏 } }); } ``` 然而,隐藏option在某些浏览器中可能不起作用(尤其是旧版本IE),另一种方法是使用disabled属性或者移除元素。但隐藏是更常见的作法,现代浏览器支持。 如果遇到浏览器兼容性问题,可以考虑将不需要的option移除并保存,但这样恢复起来需要重新添加。题目没有要求恢复,所以隐藏即可。 但是,注意:隐藏option后,用户仍然可能通过键盘上下键访问到隐藏的选项(在某些浏览器中),所以更好的做法是将其disabled或者移除。 如果需要完全禁用,可以这样: ```javascript if (personflag == 1) { $('#certTypeList option').each(function(i, o) { var value = $(o).val(); if (value == '6' || value == '9') { $(o).prop('disabled', false); // 确保启用 } else { $(o).prop('disabled', true); // 禁用 } }); } ``` 但禁用后,选项会显示为灰色,用户能看到但不能选择。如果希望完全隐藏,则用第一种方法(隐藏),并测试主要浏览器的兼容性。 根据需求,题目要求“只显示”,所以应该隐藏其他选项。所以采用第一种隐藏方式。 但是,注意:在有些浏览器中,隐藏option可能无效(比如IE8及以下),所以如果考虑兼容性,我们可以同时使用隐藏和禁用,或者直接移除。但题目没有指定浏览器,我们按现代浏览器处理。 另外,我们也可以考虑另外一种方法:在遍历之前,先隐藏所有选项,然后再显示值为6和9的选项。这样代码更简洁: ```javascript if (personflag == 1) { // 先隐藏所有选项 $('#certTypeList option').hide(); // 然后显示值为6和9的选项 $('#certTypeList option[value="6"], #certTypeList option[value="9"]').show(); } ``` 这种方法不需要each循环,效率更高。但题目要求基于each循环(因为问题中提到了each循环),所以我们还是用each循环的方式。 综上所述,我们给出两种方案,但按照题目要求,使用each循环。 最终代码: ```javascript if (personflag == 1) { $('#certTypeList option').each(function() { var $option = $(this); var val = $option.val(); if (val === "6" || val === "9") { $option.show(); } else { $option.hide(); } }); } ``` 注意:我们使用了jQuery的$(this)来获取当前选项的jQuery对象,这样更符合jQuery的链式操作习惯。 另外,如果之前有部分选项已经被隐藏,而我们需要在personflag不等于1的时候恢复,我们可以写一个恢复函数,但题目没有要求,所以不写。 如果后续需要恢复,我们可以先缓存所有option的原始状态(例如,在页面加载时记录其显示状态),或者当personflag不为1时,我们执行: ```javascript $('#certTypeList option').show(); // 显示所有选项 ``` 但这是额外的,不在本题要求范围内。 最后,确保这段代码在personflag=1时执行,并且放在文档就绪事件中($(document).ready()),以确保DOM加载完成。 示例: ```javascript $(document).ready(function() { // 假设personflag来自某个地方,比如全局变量或从服务器获取 var personflag = 1; // 测试值 if (personflag == 1) { $('#certTypeList option').each(function() { var val = $(this).val(); if (val === "6" || val === "9") { $(this).show(); } else { $(this).hide(); } }); } }); ``` 或者,如果personflag是动态变化的,我们可以将其封装成一个函数,在需要的时候调用。 根据题目要求,我们提供了基于each循环的实现。 </think>### 解决方案:根据 `personflag` 值过滤下拉选项 在 jQuery 中,当 `personflag = 1` 时,让 `certTypeList` 只显示值为 6 和 9 的选项,可通过以下步骤实现: #### 实现代码: ```javascript // 保存原始选项(用于后续恢复) const originalOptions = $("#certTypeList").html(); // 当 personflag=1 时过滤选项 if (personflag === 1) { $("#certTypeList").empty(); // 清空现有选项 // 遍历保存的原始选项 $(originalOptions).filter("option").each(function(i, o) { const value = $(o).val(); // 仅添加值为6或9的选项 if (value === "6" || value === "9") { $("#certTypeList").append(o); } }); } ``` #### 代码说明: 1. **保存原始选项** ```javascript const originalOptions = $("#certTypeList").html(); ``` 在修改前保存下拉列表的原始 HTML,用于后续恢复其他选项[^1]。 2. **条件判断** ```javascript if (personflag === 1) { ... } ``` 仅当 `personflag` 值为 1 时执行过滤操作。 3. **清空并重建选项** ```javascript $("#certTypeList").empty(); $(originalOptions).filter("option").each(...); ``` - `empty()` 清空现有选项 - 遍历原始选项集合,仅追加值为 6 或 9 的选项 4. **过滤逻辑** ```javascript if (value === "6" || value === "9") { $("#certTypeList").append(o); } ``` 通过 `value` 属性判断是否为目标选项值(6 或 9)。 #### 恢复原始状态: 当 `personflag` 不为 1 时恢复所有选项: ```javascript // 恢复原始选项 $("#certTypeList").html(originalOptions); ``` #### 备选方案(基于题目要求的 `each` 写法): ```javascript if (personflag === 1) { $("#certTypeList option").each(function(i, o) { const $option = $(o); // 隐藏非目标选项 if ($option.val() !== "6" && $option.val() !== "9") { $option.hide(); } else { $option.show(); // 确保目标选项可见 } }); } ``` > **注意**:此方法在部分浏览器中隐藏选项可能失效,推荐使用第一种重建选项的方案[^1][^2]。 --- ### 相关问题: 1. 如何保留禁用的选项(disabled)在过滤时不被移除? 2. 如何实现多条件过滤(如同时匹配值和文本内容)? 3. 当选项动态加载时,如何确保过滤逻辑正确执行[^3]? 4. 如何优化大型下拉列表(1000+选项)的过滤性能? 5. 在 Vue/React 框架中如何实现类似的选项过滤? [^1]: 引用参考 [^2]: 引用参考 [^3]: 引用参考
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值