Spring Boot之基于Redis实现MyBatis查询缓存解决方案

转载自 Spring Boot之基于Redis实现MyBatis查询缓存解决方案

1. 前言

MyBatis是Java中常用的数据层ORM框架,笔者目前在实际的开发中,也在使用MyBatis。本文主要介绍了MyBatis的缓存策略、以及基于SpringBoot和Redis实现MyBatis的二级缓存的过程。实现本文的demo,主要依赖以下软件版本信息,但是由于数据层面的实现,并不依赖具体的版本,你可以以自己主机当前的环境创建。

软件环境版本
SpringBoot1.5.18
Redis通用
MyBatis3.4.+

2. MyBatis缓存策略

2.1 一级缓存

MyBatis默认实现了一级缓存,实现过程可参考下图:

默认基础接口有两个:

  • org.apache.ibatis.session.SqlSession: 提供了用户和数据库交互需要的所有方法,默认实现类是DefaultSqlSession。

  • org.apache.ibatis.executor.Executor: 和数据库的实际操作接口,基础抽象类BaseExecutor。

我们从底层往上查看源代码,首先打开BaseExecutor的源代码,可以看到Executor实现一级缓存的成员变量是PerpetualCache对象。

/*** @author Clinton Begin*/
public abstract class BaseExecutor implements Executor {private static final Log log = LogFactory.getLog(BaseExecutor.class);protected Transaction transaction;protected Executor wrapper;protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;// 实现一级缓存的成员变量protected PerpetualCache localCache;protected PerpetualCache localOutputParameterCache;protected Configuration configuration;...
}

我们再打开PerpetualCache类的代码:

/*** @author Clinton Begin*/
public class PerpetualCache implements Cache {private final String id;private Map<Object, Object> cache = new HashMap<Object, Object>();public PerpetualCache(String id) {this.id = id;}...
}

可以看到PerpetualCache是对Cache的基本实现,而且通过内部持有一个简单的HashMap实现缓存。

了解了一级缓存的实现后,我们再回到入口处,为了你的sql语句和数据库交互,MyBatis首先需要实现SqlSession,通过DefaultSqlSessionFactory实现SqlSession的初始化的过程可查看:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// Executor初始化final Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}
}

从代码中可以看到,通过configuration创建一个Executor,实际创建Executor的过程如下:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}// 是否开启二级缓存// 如果开启,使用CahingExecutor装饰BaseExecutor的子类if (cacheEnabled) {executor = new CachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);return executor;
}

注意,cacheEnabled字段是二级缓存是否开启的标志位,如果开启,会使用使用CahingExecutor装饰BaseExecutor的子类。

创建完SqlSession,根据Statment的不同,会使用不同的SqlSession查询方法:

@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms = configuration.getMappedStatement(statement);return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}

SqlSession把具体的查询职责委托给了Executor,如果只开启了一级缓存的话,首先会进入BaseExecutor的query方法。代码如下所示:

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();}List<E> list;try {queryStack++;// 使用缓存list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482// 清空缓存clearLocalCache();}}return list;
}

query方法实现了缓存的查询过程,在query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因。

SqlSession的insert方法和delete方法,都会统一走update的流程,在BaseExecutor实现的update方法中:

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}// 清空缓存clearLocalCache();return doUpdate(ms, parameter);
}

可以看到,每次执行update方法都会执行clearLocalCache清空缓存。至此,我们分析完了MyBatis的一级缓存从入口到实现的过程。

关于MyBatis一级缓存的总结:

  • 一级缓存的生命周期和SqlSession保持一致;

  • 一级缓存的缓存通过HashMap实现;

  • 一级缓存的作用域是对应的SqlSession,假如存在多个SqlSession,写操作可能会引起脏数据。

2.2 二级缓存

在上一小节中,我们知道一级缓存的的作用域就是对应的SqlSession。若开启了二级缓存,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,二级缓存的查询流程如图所示:

二级缓存开启后,同一个namespace下的所有数据库操作语句,都使用同一个Cache,即二级缓存结果会被被多个SqlSession共享,是一个全局的变量。当开启二级缓存后,数据查询的执行流程就是二级缓存 -> 一级缓存 -> 数据库。

二级缓的实现源码,可以查看CachingExecutor类的query方法:

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 从MappedStatement中获得在配置初始化时赋予的CacheCache cache = ms.getCache();if (cache != null) {// 判断是否需要刷新缓存flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {// 主要是用来处理存储过程的ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")// 尝试从tcm中获取缓存的列表,会把获取值的职责一路传递List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

在二级缓存查询结束后,就会进入一级缓存的执行流程,可参考上一小节内容。

关于二级缓存的总结:

  • 二级缓存是SqlSession之间共享,能够做到mapper级别,并通过Cache实现缓存。

  • 由于MyBatis的缓存都是内存级的,在分布式环境下,有可能会产生脏数据,因此可以考虑使用第三方存储组件,如Redis实现二级缓存的存储,这样的安全性和性能也会更高。

3. SpringBoot和Redis实现MyBatis二级缓存

MyBatis的默认实现一级缓存的,二级缓存也是默认保存在内存中,因此当分布式部署你的应用时,有可能会产生脏数据。通用的解决方案是找第三方存储缓存结果,比如Ehcache、Redis、Memcached等。接下来,我们介绍下,使用Redis作为缓存组件,实现MyBatis二级缓存。

在实现二级缓存之前,我们假设你已经实现了SpringBoot+MyBatis的构建过程,如果还没有,建议你先创建一个demo实现简单的CRUD过程,然后再查看本文解决二级缓存的问题。

3.1 增加Redis配置

首先在你的工程加入Redis依赖:

compile('org.springframework.boot:spring-boot-starter-data-redis')

我使用的gradle,使用maven的同学可对应查询即可!

其次在配置文件中加入Redis的链接配置:

spring.redis.cluster.nodes=XXX:port,YYY:port

这里我们使用的是Redis集群配置。

打开mybatis.xml配置文件,开启二级缓存:

<setting name="cacheEnabled" value="true"/>

增加Redis的配置类,开启json的序列化:

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** Created by zhaoyh on 2019-01-23** @author zhaoyh*/
@Configuration
public class RedisConfig {/*** 重写Redis序列化方式,使用Json方式:* 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。RedisTemplate默认使用的是JdkSerializationRedisSerializer,StringRedisTemplate默认使用的是StringRedisSerializer。* Spring Data JPA为我们提供了下面的Serializer:* GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。* 在此我们将自己配置RedisTemplate并定义Serializer。* @param redisConnectionFactory* @return*/@Bean(name = "redisTemplate")public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 设置值(value)的序列化采用Jackson2JsonRedisSerializer。redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// 设置键(key)的序列化采用StringRedisSerializer。redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;}
}

3.2 实现MyBatis的Cache接口

org.apache.ibatis.cache.Cache接口是MyBatis通用的缓存实现接口,包括一级缓存和二级缓存都是基于Cache接口实现缓存机制。

创建MybatisRedisCache类,实现Cache接口:

import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;/*** Created by zhaoyh on 2019-01-22* MyBatis二级缓存配置* @author zhaoyh*/
public class MybatisRedisCache implements Cache {private static final Logger LOG = LoggerFactory.getLogger(MybatisRedisCache.class);/*** 默认redis有效期* 单位分钟*/private static final int DEFAULT_REDIS_EXPIRE = 10;/*** 注入redis*/private static RedisTemplate<String, Object> redisTemplate = null;/*** 读写锁*/private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);/*** cache id*/private String id = null;/*** 构造函数* @param id*/public MybatisRedisCache(final String id) {if (null == id) {throw new IllegalArgumentException("MybatisRedisCache Instance Require An Id...");}LOG.info("MybatisRedisCache: " + id);this.id = id;}/*** @return The identifier of this cache*/@Overridepublic String getId() {return this.id;}/*** @param key   Can be any object but usually it is a {@link}* @param value The result of a select.*/@Overridepublic void putObject(Object key, Object value) {if (null != value) {LOG.info("putObject key: " + key.toString());// 向Redis中添加数据,默认有效时间是2小时redisTemplate.opsForValue().set(key.toString(), value, DEFAULT_REDIS_EXPIRE, TimeUnit.MINUTES);}}/*** @param key The key* @return The object stored in the cache.*/@Overridepublic Object getObject(Object key) {try {if (null != key) {LOG.info("getObject key: " + key.toString());return redisTemplate.opsForValue().get(key.toString());}} catch (Exception e) {LOG.error("getFromRedis: " + key.toString() + " failed!");}LOG.info("getObject null...");return null;}/*** As of 3.3.0 this method is only called during a rollback* for any previous value that was missing in the cache.* This lets any blocking cache to release the lock that* may have previously put on the key.* A blocking cache puts a lock when a value is null* and releases it when the value is back again.* This way other threads will wait for the value to be* available instead of hitting the database.** 删除缓存中的对象** @param keyObject The key* @return Not used*/@Overridepublic Object removeObject(Object keyObject) {if (null != keyObject) {redisTemplate.delete(keyObject.toString());}return null;}/*** Clears this cache instance* 有delete、update、insert操作时执行此函数*/@Overridepublic void clear() {LOG.info("clear...");try {Set<String> keys = redisTemplate.keys("*:" + this.id + "*");LOG.info("keys size: " + keys.size());for (String key : keys) {LOG.info("key : " + key);}if (!CollectionUtils.isEmpty(keys)) {redisTemplate.delete(keys);}} catch (Exception e) {LOG.error("clear failed!", e);}}/*** Optional. This method is not called by the core.** @return The number of elements stored in the cache (not its capacity).*/@Overridepublic int getSize() {Long size = (Long) redisTemplate.execute(new RedisCallback<Long>() {@Overridepublic Long doInRedis(RedisConnection connection) throws DataAccessException {return connection.dbSize();}});LOG.info("getSize: " + size.intValue());return size.intValue();}/*** Optional. As of 3.2.6 this method is no longer called by the core.* <p>* Any locking needed by the cache must be provided internally by the cache provider.** @return A ReadWriteLock*/@Overridepublic ReadWriteLock getReadWriteLock() {return this.readWriteLock;}public static void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {MybatisRedisCache.redisTemplate = redisTemplate;}

由于redisTemplate是类变量,需要手动注入,再创建一个配置类注入redisTemplate即可:

/*** Created by zhaoyh on 2019-01-22* @author zhaoyh*/
@Component
public class MyBatisHelper {/*** 注入redis* @param redisTemplate*/@Autowired@Qualifier("redisTemplate")public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {MybatisRedisCache.setRedisTemplate(redisTemplate);}
}

3.3 mapper文件中加入二级缓存的声明

在任意需要开启二级缓存的mapper配置文件中,加入:

<!-- mapper开启二级缓存 -->
<cache type="XX.XX.MybatisRedisCache"><!-- 定义回收的策略 --><property name="eviction" value="LRU"/><!-- 配置一定时间自动刷新缓存,单位是毫秒 --><property name="flushInterval" value="600000"/><!-- 最多缓存对象的个数 --><property name="size" value="1024"/><!-- 是否只读,若配置可读写,则需要对应的实体类能够序列化 --><property name="readOnly" value="false"/>
</cache>

至此,就完成了基于Redis的MyBatis二级缓存的配置。

4. FAQ

  • 二级缓存相比较于一级缓存来说,粒度更细,但是也会更不可控,安全使用二级缓存的条件很难。

  • 二级缓存非常适合查询热度高且更新频率低的数据,请谨慎使用。

  • 建议在生产环境下关闭二级缓存,使得MyBatis单纯作为ORM框架即可,缓存使用其他更安全的策略。

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

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

相关文章

数据库架构演变概要

一&#xff0e;背景为了适应业务增长,数据库数据量快速增长&#xff0c;性能日趋下降&#xff0c;稳定性不佳的实际情况&#xff0c;急需架构逐步演变适应未来的业务发展。二&#xff0e;现状【稳定性】数据库为单点&#xff0c;没有高可用和稳定性方案。【数据量大】数据库目前…

jzoj3189-解密【字符串hash】

正题 题目大意 一个句子有多个单词。 给出了一个加密了的串。加密方法是将不同的单词转换成不同的单词。然后再给一个加密前的串&#xff0c;求再加密串中可能出现的最早位置。 解题思路 设aia_iai​表示与iii相同的前一个字母的位置。 然后根据题目意思对与两个串如果aaa序列…

面试请不要再问我Spring Cloud底层原理

转载自 面试请不要再问我Spring Cloud底层原理 概述 毫无疑问&#xff0c;Spring Cloud是目前微服务架构领域的翘楚&#xff0c;无数的书籍博客都在讲解这个技术。不过大多数讲解还停留在对Spring Cloud功能使用的层面&#xff0c;其底层的很多原理&#xff0c;很多人可能并…

nginx配置前端反向代理

本地这里有两个端口&#xff1a; 8080&#xff1a;前端页面 80&#xff1a;后端接口 在nginx配置 listen 8888;server_name 127.0.0.1;location / {root html;proxy_pass http://127.0.0.1:8080;# try_files $uri $uri/ /index.php$is_args$args;index index.html…

Visual Studio 2017的第五个更新包扩展了调试工具

Visual Studio 2017近日收到了最新的完整更新包&#xff0c;版本号为15.5。跟随前几次更新的步伐&#xff0c;这次发布提供了一系列几乎会让所有用户从中受益的特性。此次发布的一个重点是IDE的性能&#xff0c;尤其是减少C#/Visual Basic项目的加载时间。在.NET Core项目中进行…

Spring Boot 多数据源(读写分离)入门

转载自 芋道 Spring Boot 多数据源&#xff08;读写分离&#xff09;入门 1. 概述 在项目中&#xff0c;我们可能会碰到需要多数据源的场景。例如说&#xff1a; 读写分离&#xff1a;数据库主节点压力比较大&#xff0c;需要增加从节点提供读操作&#xff0c;以减少压力。 …

欢乐纪中A组周六赛【2019.3.30】

前言 做A组被虐好惨 成绩 RankRankRank是有算别人的 RankRankRankPersonPersonPersonScoreScoreScoreAAABBBCCC1010102017ZYC2017ZYC2017ZYC1141141142424244040405050501313132017XXY2017XXY2017XXY1001001000001001001000001818182017HJQ2017HJQ2017HJQ95959500095959500022…

参加双车项目的一些感触

已经十月尾旬了&#xff0c;再过两天就是11月份了&#xff0c;这个月我过的很不好&#xff0c;或者说很煎熬吧&#xff01;&#xff01;&#xff01; 国庆节之后就一直参加一个双车的项目&#xff0c;由于我对硬件算是一窍不通&#xff0c;学这个很吃力&#xff0c;相比于另一…

深港澳大湾区第三次.NET技术交流会圆满成功

2017年12月10日&#xff0c;一场以云、devops、微服务、容器是现在这个发展阶段的软件形态&#xff0c; 本次活动我们围绕这些话题介绍.NET生态下的发展本地社区活动&#xff0c;这次活动还得到如鹏网杨中科老师的大力支持开通网上直播&#xff0c;网上有229位参与活动&#xf…

P2101-命运石之门的选择【dp,离散化】

前言 我切掉这道题是命运石之门的选择 正题 题目链接:https://www.luogu.org/problemnew/show/P2101 题目大意 nnn个连在一起的高度hih_ihi​盒子。一个刷子只能直着刷而且得连续都得刷。求至少刷多少次。 解题思路 fi,jf_{i,j}fi,j​表示前iii个已经刷完了&#xff0c;上一个…

基于消息中间件RabbitMQ实现简单的RPC服务

转载自 基于消息中间件RabbitMQ实现简单的RPC服务 RPC(Remote Procedure Call,远程过程调用)&#xff0c;是一种计算机通信协议。对于两台机器而言&#xff0c;就是A服务器上的应用程序调用B服务器上的函数或者方法&#xff0c;由于不在同一个内存空间或机器上运行&#xff0c…

laravel关闭crsf

在中间件VerifyCsrfToken.php 加入 自己想要关闭的crsf protected $except [user/*,article/*,article,api/*,];

开源纯C#工控网关+组态软件(七)数据采集与归档

一、 引子在当前自动化、信息化、智能化的时代背景下&#xff0c;数据的作用日渐凸显。而工业发展到如今&#xff0c;科技含量和自动化水平均显著提高&#xff0c;但对数据的采集、利用才开始起步。对工业企业而言&#xff0c;数据采集日益受到重视&#xff0c;主要应用场景包…

nssl1167-桐人的约会【最短路】

正题 题目大意 去掉一条边使得最短路最长。 解题思路 这条边一定在最短路上而最短路最多只有n−1n-1n−1条边&#xff0c;所以直接枚举最短路上的边。复杂度O(nmK)O(nmK)O(nmK) codecodecode #include<cstdio> #include<algorithm> #include<queue> #incl…

实践出真知之Spring Cloud之基于Eureka、Ribbon、Feign的真实案例

转载自 实践出真知之Spring Cloud之基于Eureka、Ribbon、Feign的真实案例 Eureka是Spring Cloud Eureka的简称&#xff0c;是Netflix提供的组件之一。通过Eureka可以提供服务注册、发现、负载均衡、降级、熔断等功能。本篇主要介绍Eureka作为服务注册中心&#xff0c;以及实现…

从零开发一个laravel项目的增删改查、详情

环境要求&#xff1a; wampcomposer 创建laravel项目&#xff1a; composer create-project --prefer-dist laravel/laravel person快速完成person注册登录开发 1、migration php artisan make:migration create_people_table$table->increments(id);$table->string…

使用Api分析器与Windows兼容包来编写智能的跨平台.NET Core应用

本文翻译自Scott Hanselman博客&#xff1a;https://www.hanselman.com/blog/WritingSmarterCrossplatformNETCoreAppsWithTheAPIAnalyzerAndWindowsCompatibilityPack.aspx正文&#xff1a;这是最近这几周你应该知道的一对.Net Core界的优秀工具。我们在编写或者移植跨平台代码…

P4562-[JXOI2018]游戏【数论,组合数学】

正题 题目链接:https://www.luogu.org/problemnew/show/P4562 题目大意 l∼rl\sim rl∼r的变化&#xff0c;每次访问第iii个那么iii的倍数就不用访问了。对于一个顺序sss&#xff0c;定义t(s)t(s)t(s)表示按这个顺序访问玩前t(s)t(s)t(s)个就都不用访问了。求所有顺序的t(s)t(…

Redis RDB文件格式全解析

转载自 Redis RDB文件格式全解析 点评 这篇文章作为对RDB理解的教程文章&#xff0c;对RDB文件的原理理解有助于进行Redis高阶应用的设计与开发。 文章转自&#xff1a;http://blog.nosqlfan.com/html/3734.html 作者&#xff1a;nosqlfan RDB文件是Redis持久化的一种方式…

实验进行中:.NET WebAssembly支持

目前四大主流浏览器都默认支持WebAssembly&#xff0c;而.NET社区也在继续推动为.NET开发者提供相关能力&#xff0c;来将他们的代码编译成WebAssembly&#xff0c;然后在浏览器上运行。WebAssembly是一种二进制web格式&#xff0c;旨在以接近原生的性能运行不是用JavaScript语…