游戏缓存与异步持久化的完美邂逅

1、问题提出

游戏服务器,需要频繁的读取玩家数据,同时也需求频发修改玩家数据,并持久化到数据库。为了提高游戏服务器的性能,我们应该怎么处理呢?

2、应用程序缓存

缓存,是指应用程序从数据库读取完数据之后,就将数据缓存在进程内存或第三方内存(例如redis)。游戏服务器对于玩家数据的读写是非常频繁的,为了减低数据库压力,通常会使用缓存。以下是一些使用缓存的好处:

  1. 提高响应速度:缓存可以将之前计算或检索的结果存储在内存中,当下次相同的请求到达时,可以直接从缓存中获取结果,避免了重复的计算或数据库查询,从而大幅提高响应速度。

  2. 减少对资源的访问压力:缓存可以减少对资源的频繁访问,比如数据库、网络等,从而减少对这些资源的压力。这可以提高应用程序的整体性能,并降低对资源的依赖。

  3. 支持高并发:使用缓存可以缓解高并发环境下对数据库或其他资源的并发访问压力。通过将经常访问的数据存储在缓存中,可以提供更快的响应时间,并支持更高的并发请求。

3、spring使用缓存

3.1、SpringCache基本使用方法

在Spring中,可以通过Spring Cache来使用缓存。下面是使用Spring Cache的一般步骤:

  1. 添加依赖:在项目的构建文件(如pom.xml)中添加Spring Cache的相关依赖。

  2. 配置缓存管理器:在Spring的配置文件(如applicationContext.xml)中配置缓存管理器。可以选择使用Spring提供的缓存管理器实现,如ConcurrentMapCacheManager、EhCacheCacheManager等,也可以自定义缓存管理器。

  3. 在需要缓存的方法上添加缓存注解:在需要进行缓存的方法上添加Spring Cache的缓存注解,如@Cacheable、@CachePut等。这些注解可以指定缓存的名称、缓存条目的键,以及在何时加入或刷新缓存条目。

  4. 配置缓存注解的属性:根据需求,可以为缓存注解添加一些属性,如缓存的失效时间、编写缓存的键生成器等。

  5. 启用缓存功能:在Spring的配置类上使用@EnableCaching注解,以启用Spring Cache的功能

SpringCache通过注解提供缓存服务,注解只是提供一个抽象的统一访问接口,而没有提供缓存的实现。对于每个版本的spring,其使用的缓存实现存在一定的差异性。例如springboot 2.X,提供以下的缓存实现。

public enum CacheType {GENERIC,JCACHE,EHCACHE,HAZELCAST,INFINISPAN,COUCHBASE,REDIS,CAFFEINE,SIMPLE,NONE;private CacheType() {}
}

3.2、SpringCache常用注解

SpringCache最重要有以下几个注解

  1. @Cacheable:将方法的返回值缓存起来,并在下次调用时,直接从缓存中获取,而不执行方法体。

  2. @CachePut:将方法的返回值缓存起来,与@Cacheable不同的是,@CachePut会每次都执行方法体,并将返回值放入缓存中。

  3. @CacheEvict:从缓存中移除一个或多个条目。可以通过指定的key来删除具体的缓存条目,或者通过allEntries属性来删除所有的缓存条目。

3.3、使用进程缓存与进程外缓存的区别

SpringCache底层的缓存实现,即可以使用进程内缓存(例如EhCache),也可以使用进程外缓存(例如Redis)。得益于SpringCache的优秀API,在业务代码切换缓存实现,仅需修改配置文件,及针对不同缓存实现的个性化配置,使用缓存的业务代码几乎不用做任何修改。

使用进程内缓存特点:应用程序重启之后,缓存随即失效,需要重新加载。

使用进程外缓存特点:应用程序重启之后,只要redis不重启,缓存仍然生效(当然,Redis可以选择持久化,及时重启也能保存缓存数据)。对于进程外缓存,由于有对应的可视化工具,可以帮助用户加深对SpringCache的理解。

总结:对于活跃缓存数据比较多,推荐使用Redis等进程外缓存。而如果活跃缓存不是很多,直接使用进程内缓存即可。因为进程外缓存,虽然有诸多优点,但由于跨进程,甚至跨机器,需要额外使用网络io,程序与redis数据通信导致的序列化反序列化io,有大量io消耗

3.4、缓存击穿与缓存雪崩

缓存击穿:指缓存中没有数据,所有的请求都落到了数据库上,可能导致数据库压力剧增。

解决方法

  1. 游戏服务器一般会缓存所有玩家基础信息,包括uid,等级,姓名等。在读取数据的时候进行预判,过滤无效的id

  2. 缓存空对象:对查询结果为空的key,可以设置一个默认值或者空对象进行缓存,并设置一个较短的过期时间。

缓存雪崩:指缓存同一时间大量失效,导致所有请求都落到了数据库上,可能导致数据库压力剧增。

解决方法

  1. 设置XX时间没进行读写才移除缓存,例如使用Caffeine缓存,可以设置expireAfterWrite等参数,只要数据属于热门数据(近期有访问),则不会从缓存移除。

  2. 设置永不过期,如果使用redis缓存,由于redis只能设置全局ttl过期时间,无法刷新访问时间,只能选择永不过期。

3.5、SpringCache使用Redis进程外缓存

1.引入Redis相关依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.在application.properties或application.yml文件中配置Redis连接信息

##使用redis缓存
spirng.cache.type=redis##redis相关配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0

3.修改redis使用json数据格式

springcache redis默认的序列化方式基于jdk自带的序列化方式。(这里需要吐槽一下,jdk自带的序列化非常垃圾,根本没什么人使用。实体需要实现Serializable接口不说,性能又差,无法跨语言,全身上下尽是缺点)

@Configuration
public class RedisConfig  extends CachingConfigurerSupport {@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(factory);Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);MyObjectMapper objectMapper = new MyObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;}@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {RedisSerializer<String> redisSerializer = new StringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);MyObjectMapper objectMapper = new MyObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);// 配置序列化RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration).build();}private class MyObjectMapper extends ObjectMapper {private static final long serialVersionUID = 1L;public MyObjectMapper() {super();this.configure(MapperFeature.USE_ANNOTATIONS, false);// 只针对非空的值进行序列化this.setSerializationInclusion(JsonInclude.Include.NON_NULL);this.enableDefaultTyping(DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);this.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);}}}

4、封装SpringCache操作缓存

4.1、SpringCache操作缓存的坑

SpringCache默认使用代理机制来实现,如果在同一个类内部,调用有缓存注解的方法,是不会触发缓存的,例如下面的代码。

@Service
public class MyServiceImpl implements MyService {@Cacheable("myCache")@Overridepublic String getValue() {System.out.println("Getting value from getValue() method");return "myValue";}@Overridepublic void callGetValue() {// 以下代码不会触发缓存String value =  this.getValue();System.out.println("Value: " + value);}
}

有多种方法解决以上的问题,比如:

  • 使用AspectJ实现AOP(编译阶段织入),不采用默认的Proxy实现AOP。
  • 分离缓存实现与业务调用代码,数据缓存单独放在一个类,跟其他调用缓存的业务代码分离开。

本文选择第二种方式进行演示。

4.2、对缓存业务代码加一层封装

对于缓存服务,我们只关心对缓存数据进行查询,更新,删除等基本操作,不提供其他与缓存无关的业务代码,如下

public interface EntityCacheService<E extends BaseEntity<PK>, PK extends Serializable & Comparable<PK>> {/*** 根据id获取实体* @param id* @return*/E getEntity(PK id);/*** 更新/插入实体* 若实体已存在于数据库,则执行更新操作;否则,执行插入操作* @param entity* @return*/BaseEntity<PK> putEntity(E entity);/*** 移除实体* @param id* @return*/default BaseEntity<PK> removeEntity(PK id) {throw new UnsupportedOperationException();}
}

其中,BaseEntity是实体记录,主要有以下方法

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Transient;public interface BaseEntity<PK extends Serializable & Comparable<PK>> {/*** 实体唯一主键*/<PK> PK getId();/*** 查询/设置删除状态*/@Getter@Setter@Transientprivate boolean delete;}

这两个方法非常重要,与异步持久化强相关,后文详述。

针对具体数据表的缓存操作,示例代码如下

@Service
public class PlayerCacheService implements EntityCacheService<Player> {@AutowiredPlayerRepository playerRepository;@Cacheable(value = "player")@Overridepublic Code get(String id) {Optional<Player> optional = playerRepository.findById(id);System.out.println("load from db");return optional.orElse(null);}@CachePut(value = "code", key="#entity.id")@Overridepublic BaseEntity put(Player entity) {SpringContext.getDbService().saveToDb(entity);return entity;}@CacheEvict(value = "player")@Overridepublic void remove(String id) {Optional<Player> optional = playerRepository.findById(id);optional.ifPresent(SpringContext.getDbService()::deleteFromDb);}}

orm方案采用springdata jpa接口

@Repository
public interface PlayerRepository extends MongoRepository<Player, String> {}

5、异步持久化

5.1、异步持久化机制

游戏里玩家数据的变动是非常频繁的,例如连续开100个道具,在战场杀怪刷经验等,如果玩家的每一个操作都持久化到数据库,无疑对数据库的压力非常大。因此,游戏服务器采用的是异步持久化。具体来说,异步持久化有以下三种策略。

  • 基于队列:将所有需要持久化的实体进行排队,需要对重复插入的数据进行去重
  • 定时入库:以一定的频率周期性批量插入
  • 延迟入库:对每一个实体,单独延迟XX时间后再入库

读者可根据需要,综合使用上面几种策略。

如果只使用异步持久化,或者只使用缓存,无法解决下面的问题。

  • 游戏数据不仅仅频发读取,同时修改频率也非常高。如果只使用缓存,那么每次修改数据都要实时写入数据库,会导致数据库出现性能瓶颈。
  • 如果只使用异步持久化,那么一旦重新从数据库读取数据,会造成“脏读”。即,异步持久化的数据还没真正保存,新的读取操作已经开始了,这时,读取的数据是过时数据。

只有缓存与异步持久化同时使用,才能碰撞出完美火花。对于玩家数据,一旦从数据库读取之后,便保存起来,下次读取不再操作数据库。(当然,对于沉默数据,设置失效时间,避免内存爆炸)。玩家的每次操作,只修改内存,再异步持久化到数据库

5.2、异步持久化API

本文异步策略采用定时策略作为演示。持久化线程每隔XX毫秒持久化一波数据。

基本策略如下:

  • 充分利用多核处理器的优势,使用线程组进行持久化。每个持久化容器保存一个更新队列。
  • 持久化线程的run()方法是一个死循环,周期性取出数据,并进行持久化。
  • 对于在同一个周期重复加入的实体数据进行去重,由于持久化容器统一处理不同的数据表,要求所有的实体记录id全局唯一(BaseEntity的方法getId()方法发挥作用)。最简单的,可以在每个实体的id前面该实体对应的表名。
  • 充分利用orm工具的updateOrInsert机制,统一处理实体的插入/更新操作,而对于删除操作,增加一个标记字段。(BaseEntity的delete属性发挥作用)

异步持久化工具代码如下:

@Service
public class DbService {@Autowiredprivate MongoTemplate mongoTemplate;private final AtomicBoolean run = new AtomicBoolean(true);private final int WORKER_CAPACITY = Math.max(4, Runtime.getRuntime().availableProcessors()) / 2;private Worker[] workers;@PostConstructprivate void init() {workers = new Worker[WORKER_CAPACITY];NamedThreadFactory namedThreadFactory = new NamedThreadFactory("web-db-service");for (int i = 0; i < WORKER_CAPACITY; i++) {Worker worker = new Worker();workers[i] = worker;namedThreadFactory.newThread(worker).start();}}public void saveToDb(BaseEntity entity) {int index = Math.abs(entity.getId().hashCode()) % WORKER_CAPACITY;workers[index].addToQueue(entity);}public void deleteFromDb(BaseEntity entity) {entity.setDelete(true);saveToDb(entity);}public void shutDownGracefully() {for (int i = 0; i < workers.length; i++) {Worker worker = workers[i];worker.shutDown();}}@Overridepublic String toString() {Map<Integer, Integer> data = new HashMap<>();for (int i = 0; i < WORKER_CAPACITY; i++) {Worker w = workers[i];data.put(i, w.queueSize());}return JsonUtil.object2String(data);}private class Worker implements Runnable {private Map<String, BaseEntity> data = new ConcurrentHashMap<>();void addToQueue(BaseEntity ent) {this.data.put(ent.getId(), ent);}@Overridepublic void run() {while (run.get()) {try {Thread.sleep(500);} catch (InterruptedException ignore) {}if (data.isEmpty()) {continue;}// 引用替换,转移数据Map<String, BaseEntity> image = data;this.data = new ConcurrentHashMap<>();image.forEach((key, value) -> {try {// 优先执行删除操作if (value.isDelete()) {mongoTemplate.remove(value);} else {mongoTemplate.save(value);}} catch (Exception exception) {LoggerUtil.error("", exception);}});}}void shutDown() {data.forEach((key, value) -> {try {saveToDb(value);} catch (Exception exception) {LoggerUtil.error("", exception);}});}public int queueSize() {return data.size();}}
}

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

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

相关文章

基于CentOS Stream 9平台安装MySQL8.4.0 LTS

1. 安装之前 1.1 查看系统版本 [rootcoisini /]# cat /etc/redhat-release CentOS Stream release 9 1.2 查看cpu架构 [rootcoisini /]# lscpu 架构&#xff1a; x86_64 CPU 运行模式&#xff1a; 32-bit, 64-bit 2. MySQL官方下载https://dev.mysql.com/downloads/mysql/ 或…

相亲交友APP系统|婚恋交友社交软件|语音聊天平台定制开发

在现代社会&#xff0c;婚恋交友已经成为了人们日常生活中的一项重要任务。为了方便用户进行相亲交友活动&#xff0c;各种相亲交友APP系统和婚恋交友社交软件应运而生。本文将介绍相亲交友APP系统、婚恋交友社交软件的开发以及语音聊天平台的定制开发的相关知识和指导。 一、…

special characters are not allowed

处理域名连接nacos读取配置异常 1 项目启动报错2 问题处理3 刷新依赖重启问题解决 1 项目启动报错 使用ip可以正在启动&#xff0c;但是使用域名报下面的错误 2024-06-15 17:37:22.981 ERROR 29268 --- [ main] c.a.c.n.c.NacosPropertySourceBuilder : parse …

餐厅点餐系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;商品管理&#xff0c;用户管理&#xff0c;店家管理&#xff0c;广告管理 店家账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;商品管理&#xff0c;广告管…

牛客小白月赛96 解题报告 | 珂学家

前言 题解 A. 最少胜利题数 签到 n1 len(set(input())) n2 len(set(input()))if n1 < n2:n1, n2 n2, n1print (-1 if n1 6 else n1 - n2 1)B. 最少操作次数 思路: 分类讨论 只有-1,0,1,2这四种结果 特判 01, 10 n int(input()) s input()# 枚举 from collectio…

Windows10 MySQL(8.0.37)安装与配置

一、MySQL8.0.37下载 官网下载链接&#xff1a; https://dev.mysql.com/downloads/ 解压文件&#xff0c;解压到你想要的位置 二、新建MySQL配置文件 右键新建文本文档 新建my.txt文件 编辑my.txt文件&#xff0c;输入以下内容 [mysqld] # 设置 3306 端口 port3306 # 设…

nvm-windows nodejs 版本管理安装

下载和说明地址&#xff1a; GitHub - coreybutler/nvm-windows: A node.js version management utility for Windows. Ironically written in Go.

Speech JS:JavaScript 的语音识别与合成

随着人工智能和自然语言处理技术的快速发展&#xff0c;语音识别和语音合成已经成为许多应用程序的重要功能。在 Web 开发领域&#xff0c;Speech JS 是一个非常实用的工具库&#xff0c;它使得在 JavaScript 应用中实现语音识别和语音合成变得更加简便和高效。 什么是 Speech…

SQLServer使用 PIVOT 和 UNPIVOT行列转换

在SQL Server中&#xff0c;PIVOT是一个用于将行数据转换为列数据的操作。它特别适用于将多个行中的值转换为多个列的情况&#xff0c;并在此过程中执行聚合操作。以下是关于SQL Server中PIVOT操作的详细解释和示例&#xff1a; 1、本文内容 概述语法备注关键点简单 PIVOT 示…

Linux 常用命令 - userdel 【删除用户】

简介 userdel 这个命令源自于 “user delete”&#xff0c;即用户删除。这个命令主要用于在 Linux 系统中删除用户账户及其相关文件。当管理员需要移除一个用户及其在系统中的所有踪迹时&#xff0c;会用到这个命令。 使用方式 userdel [选项] 用户名常用参数 -f&#xff1a…

15.RedHat认证-Ansible自动化运维(上)

15.RedHat认证-Ansible自动化运维(上) RHCE8-RH294 Ansible自动化&#xff08;Ansible版本是2.8.2&#xff09; Ansible介绍 1.Ansible是什么&#xff1f; Ansible是一个简单的强大的无代理的自动化运维工具&#xff08;Ansible是自动化运维工具&#xff09;Ansible特点 简…

华为Atlas 300I 推理卡显卡安装

华为Atlas 300I 推理卡显卡安装 参考链接&#xff1a; https://support.huawei.com/enterprise/zh/doc/EDOC1100115618/c5bac9d1 确认操作系统 查询服务器当前运行环境的操作系统架构及版本 uname -m && cat /etc/*release获取软件包 下载对应版本的包 A800-3000-NP…

idea自动生成单元测试工具

idea自动生成单元测试工具 Squaretest插件&#xff08;收费&#xff09;TestMe插件&#xff08;免费&#xff09;启动springboot应用调用rest接口 Squaretest插件&#xff08;收费&#xff09; 1.File——>Settings——>Plugins&#xff0c;搜索Squaretest&#xff0c;然…

RPC知识

一、为什么要有RPC&#xff1a; HTTP协议的接口&#xff0c;在接口不多、系统与系统交互较少的情况下&#xff0c;解决信息孤岛初期常使用的一种通信手段&#xff1b;优点就是简单、直接、开发方便&#xff0c;利用现成的HTTP协议进行传输。 但是&#xff0c;如果是一个大型的网…

Java 对象(列表)复制【工具类】

Java当中常常会遇到对象的复制或者列表对象的复制&#xff0c;准备了一份工具类供大家参考&#xff1a; import org.springframework.beans.BeanUtils; import org.springframework.util.CollectionUtils;import java.util.ArrayList; import java.util.List; import java.uti…

[大模型]XVERSE-7B-chat FastAPI 部署

XVERSE-7B-Chat为XVERSE-7B模型对齐后的版本。 XVERSE-7B 是由深圳元象科技自主研发的支持多语言的大语言模型&#xff08;Large Language Model&#xff09;&#xff0c;参数规模为 70 亿&#xff0c;主要特点如下&#xff1a; 模型结构&#xff1a;XVERSE-7B 使用主流 Deco…

HAL库开发--STM32的HAL环境搭建

知不足而奋进 望远山而前行 目录 文章目录 前言 下载 安装 解压 安装 添加开发包 修改仓库路径 下载软件开发包&#xff08;慢&#xff0c;不推荐&#xff09; 解压已有软件开发包&#xff08;快&#xff0c;推荐&#xff09; 总结 前言 在嵌入式系统开发中&#x…

线上教育培训办公系统系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;教师管理&#xff0c;学生管理&#xff0c;运营事件管理 教师账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;作业管理&#xff0c;电…

【React】《React 学习手册 (第2版) 》笔记-Chapter4-React 运行机制

四、React 运行机制 使用 React 构建应用几乎离不开 JSX。这是一种基于标签的 JavaScript 句法&#xff0c;看起来很像 HTML。 为了在浏览器中使用 React&#xff0c;我们要引入两个库&#xff1a;React 和 ReactDOM。前者用于创建视图&#xff0c;后者则具体负责在浏览器中渲…

Java版+ SaaS应用+接口技术RESTful API 技术开发的智慧医院HIS系统源码 专注医院管理系统研发 支持二开

Java版 SaaS应用接口技术RESTful API WebSocket WebService技术开发的智慧医院HIS系统源码 专注医院管理系统研发 支持二开 医院住院管理系统&#xff08;Hospital Information System简称HIS&#xff09;是一门医学、信息、管理、计算机等多种学科为一体的边缘科学&#xff…