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

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、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 {


    @Bean
    public 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;
    }

    @Bean
    public 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")
    @Override
    public String getValue() {
        System.out.println("Getting value from getValue() method");
        return "myValue";
    }

    @Override
    public 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
    @Transient
    private boolean delete;

}

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

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

@Service
public class PlayerCacheService implements EntityCacheService<Player> {

    @Autowired
    PlayerRepository playerRepository;

    @Cacheable(value = "player")
    @Override
    public 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")
    @Override
    public BaseEntity put(Player entity) {
        SpringContext.getDbService().saveToDb(entity);
        return entity;
    }

    @CacheEvict(value = "player")
    @Override
    public 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 {

    @Autowired
    private MongoTemplate mongoTemplate;

    private final AtomicBoolean run = new AtomicBoolean(true);

    private final int WORKER_CAPACITY = Math.max(4, Runtime.getRuntime().availableProcessors()) / 2;
    private Worker[] workers;

    @PostConstruct
    private 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();
        }
    }

    @Override
    public 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);
        }

        @Override
        public 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.mfbz.cn/a/712777.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

23种设计模式之享元模式

享元模式 1、定义 享元模式&#xff1a;运用共享技术有效的支持大量细粒度对象的复用 2、享元模式结构 Flyweight&#xff08;抽象享元类&#xff09;&#xff1a;通常是一个接口或抽象类&#xff0c;在抽象享元类中声明了具体享元类公共的方法&#xff0c;这些方法可以向外…

从多线程设计模式到对 CompletableFuture 的应用

大家好&#xff0c;我是 方圆。最近在开发 延保服务 频道页时&#xff0c;为了提高查询效率&#xff0c;使用到了多线程技术。为了对多线程方案设计有更加充分的了解&#xff0c;在业余时间读完了《图解 Java 多线程设计模式》这本书&#xff0c;觉得收获良多。本篇文章将介绍其…

几种经典查找算法

几种经典查找算法 顺序查找法二分查找法判定树 二叉查找树&#xff08;BST&#xff09;索引查找B-树B树散列表&#xff08;hash&#xff09;查找 顺序查找法 顺序查找的平均查找长度为&#xff1a; 时间复杂度为0&#xff08;n&#xff09;&#xff1b; 二分查找法 int bin…

CNN学习(7):用C++实现简单不同参数的卷积模型

目录 一、参数说明和计算公式 1、符号约定 2、输出大小计算公式 二、不同类型的卷积 1、输入3*3*1&#xff0c;卷积核3*3*1&#xff0c;输出1*1*1 &#xff08;1&#xff09;实现代码 &#xff08;2&#xff09;代码说明 2、输入4*4*1&#xff0c;卷积核3*3*1&#xff…

环保评A的意义与价值

环保评A&#xff0c;这个看似简单的称谓&#xff0c;背后却蕴藏着深厚的环保理念和实践标准。在当今社会&#xff0c;环保已经成为一项全球性的议题&#xff0c;各国都在努力推动绿色发展&#xff0c;实现可持续发展目标。那么&#xff0c;环保评A究竟是全国性的认证还是地方性…

Java SSTI服务端模版注入漏洞原理与利用

文章目录 前言Velocity基础语法基础示例命令执行 靶场实践漏洞代码漏洞验证检测工具 FreeMarker基础示例漏洞示例CMS案例 Thymeleaf基础示例漏洞示例安全方案 总结 前言 SSTI&#xff08;Server Side Template Injection&#xff09;全称服务端模板注入漏洞&#xff0c;在 Jav…

开放式耳机值得入手买吗?可以对比这几款开放式耳机看看

居家办公时&#xff0c;选择一款合适的耳机能够有效地提高工作效率。入耳式耳机虽然能够有效地隔绝外界噪音&#xff0c;但长时间佩戴会对耳朵造成负担&#xff0c;甚至引发耳道感染。而头戴式耳机虽然能够提供更好的音质&#xff0c;但体积较大&#xff0c;佩戴起来不够灵活。…

PyTorch -- Batch Normalization(BN) 快速实践

Batch Normalization 可以 改善梯度消失/爆炸问题&#xff1a;前面层的梯度经过多次传递后会变得非常小(大)&#xff0c;从而导致网络收敛速度慢(不收敛)&#xff0c;应用 BN 可缓解加速网络收敛&#xff1a;BN 使得每个神经元的输入分布更加稳定减少过拟合&#xff1a;BN 可减…

求导,积分

求导公式&#xff1a; 复合函数求导法则&#xff1a;两个函数导函数的乘积. 例如&#xff1a;f(x)2x1,f(x)2,g(x)x^24x4,g(x)2x4 那么复合函数&#xff1a; g(f(x))(2x1)^24(2x1)4 把&#xff08;2x1&#xff09;看做整体,则g2(2x1)4 然后再求&#xff08;2x1&#xff09;的导函…

LeetCode | 2879.显示前三行

在 pandas 中&#xff0c;可以使用 head() 方法来读取 DataFrame 的前几行数据。如果想读取指定数量的行&#xff0c;可以在 head() 方法中传入一个参数 n&#xff0c;读取前 n 行 import pandas as pddef selectFirstRows(employees: pd.DataFrame) -> pd.DataFrame:retur…

Dictionary 字典

文章目录 一、什么是字典1.1 字典的创建方式 一、什么是字典 字典&#xff1a; 用来存储数据&#xff0c;与列表和元组不一样的是&#xff0c;字典以键值对的形式对数据进行存储&#xff0c;也就是 key 和 value。相当于 Java 中的 Map。 注意&#xff1a; 1、 key 的值不可重…

C++进阶(一)

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 前言 本篇博客是讲解函数的重载以及引用的知识点的。 文章目录 前言 1.函数重载 1.1何为函数重载 1.2函数重载的作用 1.3函数重载的实现 2.引用 2.1何为引用 2.2定义引用 2.3引用特性 2.4常引用 2…

认识一些分布函数-Frechet分布及其应用

1. 何为Frechet分布 Frechet分布也称为极值分布(EVD)类型II,用于对数据集中的最大值进行建模。它是四种常用极值分布之一。另外三种是古贝尔分布、威布尔分布和广义极值分布(Gumbel Distribution, the Weibull Distribution and the Generalized Extreme Value Distributi…

34 Debian如何配置ELK群集

作者:网络傅老师 特别提示:未经作者允许,不得转载任何内容。违者必究! Debian如何配置ELK群集 《傅老师Debian知识库系列之34》——原创 ==前言== 傅老师Debian知识库特点: 1、拆解Debian实用技能; 2、所有操作在VMware虚拟机实测完成; 3、致力于最终形成Debian知识手…

LVS-DR模式详解:提升网站性能的最佳解决方案

LVS-DR模式原理 用户请求到达Director Server&#xff1a; 用户请求到达Director Server&#xff08;负载均衡服务器&#xff09;&#xff0c;数据包首先到达内核空间的PREROUTING链。数据包源IP&#xff1a;CIP&#xff0c;目标IP&#xff1a;VIP&#xff0c;源MAC&#xff1a…

【内存管理之C语言数组】

1.栈空间上的C数组 糟糕的可用性&#xff0c;但是你将在遗留代码中见到它们 相同类型的对象的内存块 大小必须是常量表达式 第一个元素索引为0 2.指针和C数组 更奇怪的是&#xff1a;数组标识符退化为指向第一个元素的指针 3.访问数组 4.堆空间上的C数组 相同类型的对象的内…

数据库开发——并发控制(第十一章)

文章目录 前言并发执行例题一、封锁二、封锁协议三、可串行调度四、总结 学习目标&#xff1a;重点为并发控制的基本概念及几个基本协议 前言 数据库管理系统必须提供并发控制机制&#xff0c;保证事务的隔离性和一致性 并发执行例题 一、封锁 排他锁称为写锁&#xff0c;共…

智能化状态管理:自动状态流转处理模块

目录 基本背景介绍 具体实现 基本数据准备 基本数据表 状态转换常量 状态转换注解 任务处理模版 各任务实现逻辑 开启比对任务进行处理 降噪字段处理任务处理 开启业务数据比对处理 业务数据比对处理 开始核对数据生成最终报告处理 核对数据生成最终报告处理 状…

小红书教程简化版,从0开始走向专业,小红书-主理人培养计划 (13节)

课程目录 1-小红书分析与拆解.mp4 2-小红书电商玩法.mp4 3-小红书基础信息设置10_1.mp4 4-小红书如何开店&#xff1f;.mp4 5-小红书店铺设置&#xff08;1&#xff09;.mp4 5-小红书店铺设置.mp4 6-小红书笔记制作与产品发布.mp4 7-小红书运营的文案与标题.mp4 8-小红…

Spring Boot 自定义Starter

自定义starter 创建pom项目 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.ap…