缓存监控治理在游戏业务的实践和探索

作者:来自 vivo 互联网服务器团队- Wang Zhi

通过对 Redis 和 Caffeine 的缓存监控快速发现和定位问题降低故障的影响面。

一、缓存监控的背景

  • 游戏业务中存在大量的高频请求尤其是对热门游戏而言,而应对高并发场景缓存是一个常见且有效的手段。

  • 游戏业务中大量的采用远程缓存(Redis)和本地缓存(Caffeine)组合方式来应对大流量的场景。

  • 在整个缓存使用的实践过程中,基于真实线上案例和日常缓存运维痛点沉淀了一些缓存监控治理的有效案例供分享。

二、远程缓存的监控介绍

2.1 监控的方案
2.1.1 监控目的

  • 从宏观来讲监控本质目的是为了及时发现定位并解决问题,在成本可控的前提下监控维度尽可能丰富。

  • 聚焦到 Redis 的维度,除了 Server 本身的监控指标(如请求量、连接数外),还需要监控更多偏业务的指标。

  • Redis 目前最常见的问题包括:热点 Key 问题,大 Key 问题,超负载的大请求量问题。

  • 聚焦上述的问题,在基于 Redis 的原生监控指标基础上,补充更多的包含业务属性的监控。

2.1.2 监控方案

  • 目前从监控的维度进行分析,希望能做到既能针对某个 key 的热点监控,又能针对某一类相同前缀的 key 做聚合趋势监控。前者目的是发现热点 key,后者目的是从趋势维度监控缓存的实际访问量。

  • Redis 的具体 key的监控由 Redis 自研团队集成到 redis server 侧实现监控,分类上归属为 Redis Server 侧的监控,这部分不在本篇分享中具体展开。

  • Redis 的某类相同前缀的 key 的聚合监控由业务侧通过Aspect 拦截器拦截并上报埋点实现,其中 key 的设计需要遵循便于聚合的原则,分类上归属为 Redis 的业务侧的监控。

2.1.3 监控大盘

【Redis Server系统监控指标】

说明:

  • 上图监控 Redis Server 的原生指标,具体可以参考 Redis 官方文档

    http://doc.redisfans.com/server/info.html。

  • 上述指标用来评估 Redis Server 本身的负载情况,并基于此考虑是否需要横向和纵向扩容。

【Redis 业务维度前缀监控指标】

说明:

  • 上图监控的业务维度按照某类 key 的前缀进行聚合的指标,评估各类的 Redis 的 key 的读写指标。

  • 上述指标用来评估业务对 Redis 缓存使用的合理性,如发现某个前缀 key 的写入量太大(缓存应该是读多写少场景)就需要思考缓存设计的合理性。

2.2 监控的实现

  • 业务维度按照某类 key 的前缀进行聚合的功能,关键的实现逻辑包括:一类业务需要统一前缀 key 并在末尾拼接变量;通过切面拦截 redis 的读写并上报埋点。

  • 统一前缀 key 是指:如果业务A是按照用户维度进行缓存 Key 的设计,那么 Key 的形态应该是 Prefix:UserId,Prefix 是业务场景的前缀,UserId 是用户维度的动态值。

  • 切面拦截是指:针对指定的Redis操作(包括常见的 Set 等),进行拦截并匹配前缀进行埋点上报。

2.2.1 前缀 key 设计

Redis Key 的设计

public class RedisKeyConstants {public static final String    REDIS_GAMEGROUP_NEW_KEY              = "newgamegroup";public static final String    REDIS_GAMEGROUP_DETAIL_KEY          = "gamegroup:detail";public static final String    REDIS_KEY_IUNIT_STRATEGY_COUNT      = "activity:ihandler:strategy:count";public static final String    CONTENT_DISTRIBUTE_CURRENT          = "content:distribute:current";public static final String    RECOMMEND_NOTE                      = "recommend:note";
}public class RedisUtils {public static final String    COMMON_REDIS_KEY_SPLIT    = ":";public static String buildRedisKey(String key, Object... params) {if (params == null || params.length == 0) {return key;}for (Object param : params) {key += COMMON_REDIS_KEY_SPLIT + param;}return key;}
}

左右滑动查看完整代码

说明:

  • 在常量定义 RedisKeyConstants 中按照不同的业务区分了不同的业务场景的前缀 Key。

  • 在 RedisUtils#buildRedisKey 中将业务的前缀和动态变化的参数进行拼接,中间通过分隔符进行连接。

  • 分割符的引入是为了后续切面拦截时候进行逆向切割获取前缀使用。

2.2.2 监控实现

@Slf4j
@Aspect
@Order(0)
@Component
public class RedisMonitorAspect {private static final String PREFIX_CONFIG = "redis.monitor.prefix";private static final Set<String> PREFIX_SET = new HashSet<>();@Resourceprivate MonitorComponent monitorComponent;static {// 更新前缀匹配的名单String prefixValue = VivoConfigManager.getString(PREFIX_CONFIG, "");refreshConf(prefixValue);// 增加配置变更的回调VivoConfigManager.addListener(new VivoConfigListener() {@Overridepublic void eventReceived(PropertyItem propertyItem, ChangeEventType changeEventType) {if (StringUtils.equalsIgnoreCase(propertyItem.getName(), PREFIX_CONFIG)) {refreshConf(propertyItem.getValue());}}});}/*** 更新前缀匹配的名单* @param prefixValue*/private static void refreshConf(String prefixValue) {if (StringUtils.isNotEmpty(prefixValue)) {String[] prefixArr = StringUtils.split(prefixValue, ",");Arrays.stream(prefixArr).forEach(item -> PREFIX_SET.add(item));}}@Pointcut("execution(* com.vivo.joint.dal.common.redis.dao.RedisDao.set*(..))")public void point() {}@Around("point()")public Object around(ProceedingJoinPoint pjp) throws Throwable {//业务逻辑异常情况直接抛到业务层处理Object result = pjp.proceed();try {if (VivoConfigManager.getBoolean("joint.center.redis.monitor.switch", true)) {Object[] args = pjp.getArgs();if (null != args && args.length > 0) {String redisKey = String.valueOf(args[0]);if (VivoConfigManager.getBoolean("joint.center.redis.monitor.send.log.switch", true)) {LOGGER.info("更新redis的缓存 {}", redisKey);}String monitorKey = null;// 先指定前缀匹配if (!PREFIX_SET.isEmpty()) {for (String prefix : PREFIX_SET) {if (StringUtils.startsWithIgnoreCase(redisKey, prefix)) {monitorKey = prefix;break;}}}if (StringUtils.isEmpty(monitorKey) && StringUtils.contains(redisKey, ":")) {// 需要考虑前缀的格式,保证数据写入不能膨胀monitorKey = StringUtils.substringBeforeLast(redisKey, ":");}monitorComponent.sendRedisMonitorData(monitorKey);}}} catch (Exception e) {}return result;}
}
printf("hello world!");

说明:

  • 通过 Aspect 的切面功能对 Redis 的指定操作进行拦截,如上图中的 Set 操作等,可以按需扩展到其他操作(包括 get 命令等)。

  • 针对前缀 key 的提取支持两个维度,默认场景和自定义场景,其中处理优先级为 自定义场景 > 默认场景

  • 默认场景是指如 Redis 的 Key 为 A:B:C:UserId,从后往前寻找后向第一个分割符进行分割,A:B:C:UserId 分割后的根据前缀 A:B:C 进行聚合后数据埋点上报。

  • 自定义场景如 Redis的 Key 为 A:B:UserId,通过配置自定义的前缀 A:B 来匹配,A:B:C:UserId 根据自定义的前缀分割后根据前缀 A:B 进行聚合后数据埋点上报。

  • 考虑自定义场景的灵活性,相关的自定义前缀通过配置中心实时生效。

2.3 监控的案例

public static final String REDISKEY_USER_POPUP_PLAN = "popup:user:plan";public PopupWindowPlan findPlan(FindPlanParam param) {String openId = param.getOpenId();String imei = param.getImei();String gamePackage = param.getGamePackage();Integer planType = param.getPlanType();String appId = param.getAppId();// 1、获取缓存的数据PopupWindowPlan cachedPlan = getPlanFromCache(openId, imei, gamePackage, planType);if (cachedPlan != null) {monitorPopWinPlan(cachedPlan);return cachedPlan;}// 2、未命中换成后从持久化部分获取对应的 PopupWindowPlan 对象// 3、保存到Redis换成setPlanToCache(openId, imei, gamePackage, plan);return cachedPlan;}// 从缓存中获取数据的逻辑private PopupWindowPlan getPlanFromCache(String openId, String imei, String gamePackage, Integer planType) {String key = RedisUtils.buildRedisKey(RedisKeyConstants.REDISKEY_USER_POPUP_PLAN, openId, imei, gamePackage, planType);String cacheValue = redisDao.get(key);if (StringUtils.isEmpty(cacheValue)) {return null;}try {PopupWindowPlan plan = objectMapper.readValue(cacheValue, PopupWindowPlan.class);return plan;} catch (Exception e) {}return null;}// 保存数据到缓存当中private void setPlanToCache(String openId, String imei, String gamePackage, PopupWindowPlan plan, Integer planType) {String key = RedisUtils.buildRedisKey(RedisKeyConstants.REDISKEY_USER_POPUP_PLAN, openId, imei, gamePackage, planType);try {String serializedStr = objectMapper.writeValueAsString(plan);redisDao.set(key, serializedStr, VivoConfigManager.getInteger(ConfigConstants.POPUP_PLAN_CACHE_EXPIRE_TIME, 300));} catch (Exception e) {}}

左右滑动查看完整代码

说明:

  • 如监控实现部分所述,通过 Redis Key 的前缀聚合监控,能够发现某一类业务场景的 Redis 的写请求数,进而发现 Redis 的无效使用场景。

  • 上述案例是典型的Redis的缓存使用场景:1.访问 Redis 缓存;2.若命中则直接返回结果;3、如未命中则查询持久化存储获取数据并写入 Redis 缓存。

  • 从业务监控的大盘发现前缀 popup:user:plan 存在大量的 set 操作命令,按照缓存读多写少的原则,该场景标明该缓存的设计是无效的。

  • 通过业务分析后,发现在游戏的业务场景中 用户维度+游戏维度 不存在5分钟重复访问缓存的场景,确认缓存的无效。

  • 确认缓存无效后,删除相关的缓存逻辑,降低了 Redis Server 的负载后并进一步提升了接口的响应时间。

三、本地缓存的监控介绍

3.1 监控的方案

3.1.1 监控目的

  • 从宏观来讲监控本质目的是为了及时发现定位并解决问题,在成本可控的前提下监控维度尽可能丰富。

  • 聚焦到 Caffeine 的维度,监控指标包括缓存的请求次数、命中率,未命中率等指标。

  • Caffeine 目前最常见的问题是:缓存设置不合理导致缓存穿透引发的系统问题。

3.1.2 监控方案

  • 目前从监控的维度进行分析,按照机器维度+缓存实例进行监控指标采集,其中监控指标的采集基于 Caffeine 的 recordStats 功能开启。

  • 基于 caffeine 的原生能力定制的 vivo-caffeine 集成了单机器维度+单缓存实例的指标数据的采集和上报功能。

  • vivo-caffeine 上报的数据会按照单机器+单缓存实例维度进行大盘展示,支持全量指标的查询功能。

  • vivo-caffeine 的上报的数据和公司级的告警功能相结合,例如针对缓存未命中率进行监控就能很快发现缓存穿透的问题。

3.1.3 监控大盘

【Caffeine 系统监控指标】

说明:

  • vivo-caffeine 按照单机器 + 缓存实例维度进行监控数据的上报并进行展示。

  • 所有的系统指标都支持查询并以图片的形式进行展示。

3.2 监控的实现

public final class Caffeine<K, V> {/*** caffeine的实例名称*/String instanceName;/*** caffeine的实例维护的Map信息*/static Map<String, Cache> cacheInstanceMap = new ConcurrentHashMap<>();@NonNullpublic <K1 extends K, V1 extends V> Cache<K1, V1> build() {requireWeightWithWeigher();requireNonLoadingCache();@SuppressWarnings("unchecked")Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;Cache localCache =  isBounded() ? new BoundedLocalCache.BoundedLocalManualCache<>(self) : new UnboundedLocalCache.UnboundedLocalManualCache<>(self);if (null != localCache && StringUtils.isNotEmpty(localCache.getInstanceName())) {cacheInstanceMap.put(localCache.getInstanceName(), localCache);}return localCache;}
}static Cache<String, List<String>> accountWhiteCache = Caffeine.newBuilder().applyName("accountWhiteCache").expireAfterWrite(VivoConfigManager.getInteger("trade.account.white.list.cache.ttl", 10), TimeUnit.MINUTES).recordStats().maximumSize(VivoConfigManager.getInteger("trade.account.white.list.cache.size", 100)).build();

左右滑动查看完整代码

说明:

  • Caffeine 的按缓存实例进行指标采集的前提是需要全局维护缓存实例和对应的 instanceName 之间的关联关系。

  • Caffeine 在缓存创建的时候会设置实例的名称,通过 applyName 方法设置实例名称。

public static StatsData getCacheStats(String instanceName) {Cache cache = Caffeine.getCacheByInstanceName(instanceName);CacheStats cacheStats = cache.stats();StatsData statsData = new StatsData();statsData.setInstanceName(instanceName);statsData.setTimeStamp(System.currentTimeMillis()/1000);statsData.setMemoryUsed(String.valueOf(cache.getMemoryUsed()));statsData.setEstimatedSize(String.valueOf(cache.estimatedSize()));statsData.setRequestCount(String.valueOf(cacheStats.requestCount()));statsData.setHitCount(String.valueOf(cacheStats.hitCount()));statsData.setHitRate(String.valueOf(cacheStats.hitRate()));statsData.setMissCount(String.valueOf(cacheStats.missCount()));statsData.setMissRate(String.valueOf(cacheStats.missRate()));statsData.setLoadCount(String.valueOf(cacheStats.loadCount()));statsData.setLoadSuccessCount(String.valueOf(cacheStats.loadSuccessCount()));statsData.setLoadFailureCount(String.valueOf(cacheStats.loadFailureCount()));statsData.setLoadFailureRate(String.valueOf(cacheStats.loadFailureRate()));Optional<Eviction> optionalEviction = cache.policy().eviction();optionalEviction.ifPresent(eviction -> statsData.setMaximumSize(String.valueOf(eviction.getMaximum())));Optional<Expiration> optionalExpiration = cache.policy().expireAfterWrite();optionalExpiration.ifPresent(expiration -> statsData.setExpireAfterWrite(String.valueOf(expiration.getExpiresAfter(TimeUnit.SECONDS))));optionalExpiration = cache.policy().expireAfterAccess();optionalExpiration.ifPresent(expiration -> statsData.setExpireAfterAccess(String.valueOf(expiration.getExpiresAfter(TimeUnit.SECONDS))));optionalExpiration = cache.policy().refreshAfterWrite();optionalExpiration.ifPresent(expiration -> statsData.setRefreshAfterWrite(String.valueOf(expiration.getExpiresAfter(TimeUnit.SECONDS))));return statsData;
}

左右滑动查看完整代码

说明:

  • 监控指标的采集基于 Caffeine 原生的统计功能 CacheStats。

  • 所有采集的指标封装成一个统计对象 StatsData 进行上报。

public static void sendReportData() {try {if (!VivoConfigManager.getBoolean("memory.caffeine.data.report.switch", true)) {return;}// 1、获取所有的cache实例对象Method listCacheInstanceMethod = HANDLER_MANAGER_CLASS.getMethod("listCacheInstance", null);List<String> instanceNames = (List)listCacheInstanceMethod.invoke(null, null);if (CollectionUtils.isEmpty(instanceNames)) {return;}String appName = System.getProperty("app.name");String localIp = getLocalIp();String localPort = String.valueOf(NetPortUtils.getWorkPort());ReportData reportData = new ReportData();InstanceData instanceData = new InstanceData();instanceData.setAppName(appName);instanceData.setIp(localIp);instanceData.setPort(localPort);// 2、遍历cache实例对象获取缓存监控数据Method getCacheStatsMethod = HANDLER_MANAGER_CLASS.getMethod("getCacheStats", String.class);Map<String, StatsData> statsDataMap = new HashMap<>();instanceNames.stream().forEach(instanceName -> {try {StatsData statsData = (StatsData)getCacheStatsMethod.invoke(null, instanceName);statsDataMap.put(instanceName, statsData);} catch (Exception e) {}});// 3、构建上报对象reportData.setInstanceData(instanceData);reportData.setStatsDataMap(statsDataMap);// 4、发送Http的POST请求HttpPost httpPost = new HttpPost(getReportDataUrl());httpPost.setConfig(requestConfig);StringEntity stringEntity = new StringEntity(JSON.toJSONString(reportData));stringEntity.setContentType("application/json");httpPost.setEntity(stringEntity);HttpResponse response = httpClient.execute(httpPost);String result = EntityUtils.toString(response.getEntity(),"UTF-8");EntityUtils.consume(response.getEntity());logger.info("Caffeine 数据上报成功 URL {} 参数 {} 结果 {}", getReportDataUrl(), JSON.toJSONString(reportData), result);} catch (Throwable throwable) {logger.error("Caffeine 数据上报失败 URL {} ", getReportDataUrl(), throwable);}
}

左右滑动查看完整代码

说明:

  • 每个应用单独的部署的服务作为一个采集点,进行指标的采集和上报。

  • 采集过程是获取当前部署的应用下的所有缓存实例并进行指标的采集封装。

  • 整体上报采用 Http 协议进行上报并最终展示到监控平台。

3.3 监控的案例

说明:

  • 某次线上问题发生时发现突然多出了大量的 Redis 的请求,但是无法具体定位请求的 Redis 的前缀 key,设想如果接入了 Redis 的业务监控,问题来源就能很快定位。

  • 在后续的问题排查中发现某个 Caffeine 的本地缓存因为大小设置过小导致大量的本地请求缓存穿透导致 Redis 的请求量突增,最终导致 Redis 的服务接近崩溃。

  • 针对本地缓存穿透的场景,如果采用 Caffeine 的本地缓存监控方案,能够从缓存的命中率指标和缓存的未命中率指标突增突降中发现问题根源。

四、结束语

  • 本篇内容是基于线上真实案例分享游戏业务侧在缓存监控治理方面的有效实践,监控治理本身是一个未雨绸缪的过程。在没有线上问题发生时看似不重要,但一旦发生无法快速定位问题又会导致问题的放大,因此完善的缓存监控整理其实是非常有必要的。

  • Redis 的前缀 key 的监控思路是游戏业务服务端在优化Redis 的使用效率的过程中发现的一个较好的实践,逐步延伸后发现这是一个很好的监控手段,能够通过突增的趋势快速定位问题。

  • 基于 Caffeine 的原生能力定制的监控指标采集是游戏业务服务端在探索 Caffeine 可视化过程中进行的一个探索落地,将整个缓存实例的运行态进行完整呈现,为业务稳定性贡献力量。

  • 相信业内同仁会有更多更好的实践,相互分享共同进步,共勉。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/39046.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

城市街拍人像自拍电影风格Lr调色教程,手机滤镜PS+Lightroom预设下载!

调色教程 城市街拍人像自拍的电影风格 Lr 调色&#xff0c;是利用 Adobe Lightroom 软件&#xff0c;对在城市街景中拍摄的人像自拍照片进行后期处理&#xff0c;使其呈现出电影画面般独特的视觉质感与艺术氛围。通过一系列调色操作&#xff0c;改变照片的色彩、明暗、对比等元…

爬虫案例-爬取某站视频

文章目录 1、下载FFmpeg2、爬取代码3、效果图 1、下载FFmpeg FFmpeg是一套可以用来记录、转换数字音频、视频&#xff0c;并能将其转化为流的开源计算机程序。 点击下载: ffmpeg 安装并配置 FFmpeg 步骤&#xff1a; 1.下载 FFmpeg&#xff1a; 2.访问 FFmpeg 官网。 3.选择 Wi…

LeetCode热题100精讲——Top3:最长连续序列【哈希】

你好&#xff0c;我是安然无虞。 文章目录 题目背景最长连续序列C解法Python解法 题目背景 如果大家对于 哈希 类型的概念并不熟悉, 可以先看我之前为此专门写的算法详解: 蓝桥杯算法竞赛系列第九章巧解哈希题&#xff0c;用这3种数据类型足矣 最长连续序列 题目链接&#x…

pyecharts在jupyter notebook中不能够渲染图表问题。

在使用jupyter notebook中使用pyecharts绘制可视化图表的时候,发现图表不能渲染到页面中,生成的html是没问题的,本文主要解决在jupyter notebook中不能渲染这个问题。 1、原因分析 2、解决办法 如果是使用的虚拟环境,需要下你提前激活虚拟环境,再进行下列操作。 因为需要…

使用python numpy计算并显示音频数据的频谱信息

一 概念 最近需要用到这个数据。笔者需要&#xff0c;使用 Python 的numpy库结合scipy和matplotlib库来计算并显示音频数据频谱信息的示例代码。我们将使用scipy.io.wavfile来读取音频文件&#xff0c;numpy进行快速傅里叶变换&#xff08;FFT&#xff09;计算频谱&#xff0…

算法刷题整理合集(七)·【算法赛】

本篇博客旨在记录自已的算法刷题练习成长&#xff0c;里面注有详细的代码注释以及和个人的思路想法&#xff0c;希望可以给同道之人些许帮助。本人也是算法小白&#xff0c;水平有限&#xff0c;如果文章中有什么错误或遗漏之处&#xff0c;望各位可以在评论区指正出来&#xf…

[免费]SpringBoot+Vue城市交通管理系统【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的SpringBootVue城市交通管理系统&#xff0c;分享下哈。 项目视频演示 【免费】SpringBootVue城市交通管理系统 Java毕业设计_哔哩哔哩_bilibili 项目介绍 城市交通管理系统的目的是让使用者可以更方便的将…

同旺科技USB to I2C 适配器 ---- 指令循环发送功能

所需设备&#xff1a; 内附链接 1、同旺科技USB to I2C 适配器 1、周期性的指令一次输入&#xff0c;即可以使用 “单次发送” 功能&#xff0c;也可以使用 “循环发送” 功能&#xff0c;大大减轻发送指令的编辑效率&#xff1b; 2、 “单次发送” 功能&#xff0c;“发送数据…

SQL Server——表数据的插入、修改和删除

目录 一、引言 二、表数据的插入、修改和删除 &#xff08;一&#xff09;方法一&#xff1a;在SSMS控制台上进行操作 1.向表中添加数据 2.对表中的数据进行修改 3.对表中的数据进行删除 &#xff08;二&#xff09;方法二&#xff1a;使用 SQL 代码进行操作 1.向表中添…

【MySQL】存储过程

目录 基本概念存储过程操作定义存储过程变量定义局部变量用户变量系统变量全局变量会话变量 参数传递in 关键字out 关键字inout 关键字 流程控制判断分支语句 if分支语句 case 循环循环语句 while循环语句 repeat循环语句 loop 游标异常处理 存储函数 基本概念 概述 MySQL 5.…

大数据学习(77)-Hive详解

&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一…

一种很新的“工厂”打开方式---智慧工厂

随着信息技术的不断进步&#xff0c;特别是数字化、网络化、智能化技术的快速发展&#xff0c;传统的工厂管理模式已经难以满足现代企业对于生产效率、安全管理以及决策支持等方面的需求&#xff0c;智能制造已成为全球制造业发展的主流趋势。 由于工厂实时数据的多样性、复杂性…

基于python的租房数据分析系统(爬虫爬取真实数据)

项目介绍 本租房数据分析系统具备创新爬虫功能&#xff0c;能从安居客实时抓取房屋信息&#xff0c;同时提供全面的用户管理、个人中心服务。系统支持房屋信息的新增、修改、删除、查询及用户评论&#xff0c;以及租房数据的全面管理分析。此外&#xff0c;房屋资讯管理和轮播图…

Java——ArrayList集合

ArrayList&#xff1a;基于动态数组实现&#xff0c;支持随机访问&#xff0c;适合频繁的随机访问操作&#xff0c;但在插入和删除元素时性能较差。 技术层面介绍 所属类库&#xff1a;ArrayList 位于 java.util 包中&#xff0c;它实现了 List 接口&#xff0c;因此具备 Lis…

【Linux】线程库

一、线程库管理 tid其实是一个地址 void* start(void* args) {const char* name (const char *)args;while(true){printf("我是新线程 %s &#xff0c;我的地址&#xff1a;0x%lx\n",name,pthread_self());sleep(1);}return nullptr; }int main() {pthread_t tid…

智能宠物饮水机WTL580微波雷达感应模块方案;便捷管理宠物饮水

一&#xff1a;宠物智能饮水与技术创新 1&#xff1a;非接触式感应 微波雷达模块实时检测宠物靠近行为&#xff0c;当宠物进入感应范围时&#xff0c;饮水机自动启动水泵&#xff0c;提供新鲜水流 2&#xff1a;多模式配置 感应距离&#xff1a;30-150cm可调&#xff0c;适应…

How to share files with Windows via samba in Linux mint 22

概述 Windows是大家日常使用最多的操作系统&#xff0c;在Windows主机之间&#xff0c;可以共享文件&#xff0c;那么如何在Windows主机与Linux主机之间共享文件呢&#xff1f; 要在Windows主机与Linux主机之间共享文件&#xff0c;我们可以借助Samba协议完成。借助Samba协议…

牛客周赛84 题解 Java ABCDE 仅供参考

A 小苯跑外卖 除一下看有没有余数 有余数得多一天 没余数正好 // github https://github.com/Dddddduo // github https://github.com/Dddddduo/acm-java-algorithm // github https://github.com/Dddddduo/Dduo-mini-data_structure import java.util.*; import java.io.*…

基于SpringBoot + Vue 的图书馆座位预约系统

SpringBoot 图书馆座位预约管理系统 自习室座位预约管理系统 javaSpringbootVUEredis 1. 开发环境&#xff1a; idea/eclipse、jdk1.8、maven、nodejs 2. 技术栈&#xff1a;java、springboot、Redis、mybatis、vue 3. 数据库&#xff1a; MySQL 有用户和管理员两个角色…

深入理解 lt; 和 gt;:HTML 实体转义的核心指南!!!

&#x1f6e1;️ 深入理解 < 和 >&#xff1a;HTML 实体转义的核心指南 &#x1f6e1;️ 在编程和文档编写中&#xff0c;< 和 > 符号无处不在&#xff0c;但它们也是引发语法错误、安全漏洞和渲染混乱的头号元凶&#xff01;&#x1f525; 本文将聚焦 <&#…