MyBatis缓存和二级缓存整合Redis

  • ⼀级缓存
    • 缓存验证
      • 在⼀个sqlSession中,对user表根据username进⾏两次查询,查看他们发出sql语句的情况
      • 同样是对user表进⾏两次查询,只不过两次查询之间进⾏了⼀次update操作。
      • 总结
    • ⼀级缓存原理探究与源码分析
  • ⼆级缓存
    • 开启⼆级缓存
    • 测试
      • 测试⼆级缓存和sqlSession无关
      • 测试执⾏commit()操作,⼆级缓存数据清空
    • useCache和flushCache
  • ⼆级缓存整合Redis
    • pom⽂件
    • 配置⽂件Mapper.xml
    • redis.properties
    • 测试
    • 源码分析

⼀级缓存

缓存验证

在⼀个sqlSession中,对user表根据username进⾏两次查询,查看他们发出sql语句的情况

@Testpublic void test1() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);//根据 sqlSessionFactory 产⽣ sessionSqlSession sqlSession = sqlSessionFactory.openSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);//模拟条件userUser condition = new User();//condition.setId(1);condition.setUsername("zjq");//第⼀次查询,发出sql语句,并将查询出来的结果放进缓存中List<User> userList = mapper.findByCondition(condition);System.out.println("第一次查询结果:" + userList);//第⼆次查询,由于是同⼀个sqlSession,会在缓存中查询结果//如果有,则直接从缓存中取出来,不和数据库进⾏交互List<User> userList2 = mapper.findByCondition(condition);System.out.println("第二次查询结果:" + userList2);}

查看控制台打印情况:

看控制台输出可以看出来,第一次执行了SQL查询,第二次直接打印的结果集,没有查询数据库。

同样是对user表进⾏两次查询,只不过两次查询之间进⾏了⼀次update操作。

@Testpublic void test2() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);//根据 sqlSessionFactory 产⽣ sessionSqlSession sqlSession = sqlSessionFactory.openSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);//模拟条件userUser condition = new User();//condition.setId(1);condition.setUsername("zjq");//第⼀次查询,发出sql语句,并将查询出来的结果放进缓存中List<User> userList = mapper.findByCondition(condition);System.out.println("第一次查询结果:" + userList);User user = new User();user.setId(3);user.setUsername("zjq666");user.setPassword("6566666666");//进⾏了⼀次更新操作,sqlSession.commit()mapper.updateById(user);sqlSession.commit();//第⼆次查询,由于是同⼀个sqlSession,会在缓存中查询结果//如果有,则直接从缓存中取出来,不和数据库进⾏交互List<User> userList2 = mapper.findByCondition(condition);System.out.println("第二次查询结果:" + userList2);sqlSession.close();}

查看控制台打印情况:

总结

  1. 第⼀次发起查询⽤户username为zjq的⽤户信息,先去找缓存中是否有username为zjq的⽤户信息,如果没有,从 数据库查询⽤户信息。得到⽤户信息,将⽤户信息存储到⼀级缓存中。
  2. 如果中间sqlSession去执⾏commit操作(执⾏插⼊、更新、删除),则会清空SqlSession中的一级缓存,这样做的⽬的为了让缓存中存储的是最新的信息,避免脏读。
  3. 第⼆次发起查询⽤户username为zjq的⽤户信息,先去找缓存中是否有username为zjq的⽤户信息,缓存中有,直接从缓存中获取⽤户信息。

⼀级缓存原理探究与源码分析

⼀级缓存到底是什么?⼀级缓存什么时候被创建、⼀级缓存的⼯作流程是怎样的?
上⾯我们⼀直提到⼀级缓存,那么提到⼀级缓存就绕不开SqlSession,所以索性我们就直接从SqlSession,看看有没有创建缓存或者与缓存有关的属性或者⽅法:

发现上述所有⽅法中,好像只有clearCache()和缓存沾点关系,那么就直接从这个⽅ 法⼊⼿吧,分析源码时,我们要看它(此类)是谁,它的⽗类和⼦类分别⼜是谁,对如上关系了解了,你才 会对这个类有更深的认识,分析了⼀圈,你可能会得到如下这个流程图:

public class PerpetualCache implements Cache {  private Map<Object, Object> cache = new HashMap<Object, Object>();  @Overridepublic void clear() {cache.clear();}
}

再深⼊分析,流程⾛到Perpetualcache中的clear()⽅法之后,会调⽤其cache.clear()⽅法,那么这个cache是什么东⻄呢?点进去发现,cache其实就是private Map cache = new HashMap();也就是⼀个Map,所以说cache.clear()其实就是map.clear(),也就是说,缓存其实就是本地存放的⼀个map对象,每⼀个SqISession都会存放⼀个map对象的引⽤,那么这个cache是何时创建的呢?

你觉得最有可能创建缓存的地⽅是哪⾥呢?我觉得是Executor,为什么这么认为?因为Executor是执⾏器,⽤来执⾏SQL请求,⽽且清除缓存的⽅法也在Executor中执⾏,所以很可能缓存的创建也很有可能在Executor中,看了⼀圈发现Executor中有⼀个createCacheKey⽅法,这个⽅法很像是创建缓存的⽅法,跟进去看看,你发现createCacheKey⽅法是由BaseExecutor执⾏的,代码如下

CacheKey cacheKey = new CacheKey();
//MappedStatement 的 id
// id就是Sql语句的所在位置包名+类名+ SQL名称
cacheKey.update(ms.getId());
// offset 就是 0
cacheKey.update(rowBounds.getOffset());
// limit 就是 Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit());
//具体的SQL语句
cacheKey.update(boundSql.getSql());
//后⾯是update 了 sql中带的参数
cacheKey.update(value);
...
if (configuration.getEnvironment() != null) {// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}

创建缓存key会经过⼀系列的update⽅法,udate⽅法由⼀个CacheKey这个对象来执⾏的,这个
update⽅法最终由updateList的list来把五个值存进去,对照上⾯的代码和下⾯的图示,你应该能 理解
这五个值都是什么了。

这⾥需要注意⼀下最后⼀个值,configuration.getEnvironment().getId()这是什么,这其实就是 定义在
mybatis-config.xml中的标签,如下。

<environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="${jdbc.driver}"/><property name="url" value="${jdbc.url}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/></dataSource></environment>
</environments>

**那么创建完缓存之后该⽤在何处呢?**总不会凭空创建⼀个缓存不使⽤吧?绝对不会的,经过我们对⼀级缓存的探究之后,我们发现⼀级缓存更多是⽤于查询操作,毕竟⼀级缓存也叫做查询缓存吧,为什么叫查询缓存我们⼀会⼉说。我们先来看⼀下这个缓存到底⽤在哪了,我们跟踪到org.apache.ibatis.executor.BaseExecutor的query⽅法,如下:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds
rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);//创建缓存CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds
rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {...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);
}
...
}
// queryFromDatabase ⽅法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql
boundSql) throws SQLException {List<E> list;localCache.putObject(key, EXECUTION_PLACEHOLDER);try {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {localCache.removeObject(key);}localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;
}

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localCache进⾏写⼊。 localCache对象的put⽅法最终交给Map进⾏存放。

⼆级缓存

⼆级缓存的原理和⼀级缓存原理⼀样,第⼀次查询,会将数据放⼊缓存中,然后第⼆次查询则会直接去缓存中取。但是⼀级缓存是基于sqlSession的,⽽⼆级缓存是基于mapper⽂件的namespace的,也就是说多个sqlSession可以共享⼀个mapper中的⼆级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执⾏sql查询到的数据也将存在相同的⼆级缓存区域中

如何使用二级缓存

开启⼆级缓存

和⼀级缓存默认开启不⼀样,⼆级缓存需要我们⼿动开启
⾸先在全局配置⽂件sqlMapConfig.xml⽂件中加⼊如下代码:

<!--开启⼆级缓存-->
<settings><setting name="cacheEnabled" value="true"/>
</settings>

其次在UserMapper.xml⽂件中开启缓存

<!--开启⼆级缓存-->
<cache></cache>

我们可以看到mapper.xml⽂件中就这么⼀个空标签,其实这⾥可以配置,PerpetualCache这个类是mybatis默认实现缓存功能的类。我们不写type就使⽤mybatis默认的缓存,也可以去实现Cache接⼝来⾃定义缓存。

public class PerpetualCache implements Cache {private final String id;private MapcObject, Object> cache = new HashMap();public PerpetualCache(String id) {this.id = id;}
}
public class User implements Serializable(//⽤户IDprivate int id;//⽤户姓名private String username;//⽤户性别private String sex;
}

开启了⼆级缓存后,还需要将要缓存的pojo实现Serializable接⼝,为了将缓存数据取出执⾏反序列化操
作,因为⼆级缓存数据存储介质多种多样,不⼀定只存在内存中,有可能存在硬盘中,如果我们要再取
这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接⼝。

测试

测试⼆级缓存和sqlSession无关
@Test
public void testTwoCache(){//根据 sqlSessionFactory 产⽣ sessionSqlSession sqlSession1 = sessionFactory.openSession();SqlSession sqlSession2 = sessionFactory.openSession();UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class );UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class );//第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中User u1 = userMapper1.selectUserByUserId(1);System.out.println(u1);sqlSession1.close(); //第⼀次查询完后关闭 sqlSession//第⼆次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句User u2 = userMapper2.selectUserByUserId(1);System.out.println(u2);sqlSession2.close();
}

可以看出上⾯两个不同的sqlSession,第⼀个关闭了,第⼆次查询依然不发出sql查询语句。

测试执⾏commit()操作,⼆级缓存数据清空
@Test
public void testTwoCache(){//根据 sqlSessionFactory 产⽣ sessionSqlSession sqlSession1 = sessionFactory.openSession();SqlSession sqlSession2 = sessionFactory.openSession();SqlSession sqlSession3 = sessionFactory.openSession();UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class );UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class );UserMapper userMapper3 = sqlSession2.getMapper(UserMapper. class );//第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中User u1 = userMapperl.selectUserByUserId( 1 );System.out.println(u1);sqlSessionl .close(); //第⼀次查询完后关闭sqlSession//执⾏更新操作,commit()u1.setUsername( "zjq" );userMapper3.updateUserByUserId(u1);sqlSession3.commit();//第⼆次查询,由于上次更新操作,缓存数据已经清空(防⽌数据脏读),这⾥必须再次发出sql语句User u2 = userMapper2.selectUserByUserId( 1 );System.out.println(u2);sqlSession2.close();
}

useCache和flushCache

mybatis中还可以配置userCache和flushCache等配置项,userCache是⽤来设置是否禁⽤⼆级缓存的,在statement中设置useCache=false可以禁⽤当前select语句的⼆级缓存,即每次查询都会发出 sql去查询,默认情况是true,即该sql使⽤⼆级缓存。

<select id="selectUserByUserId" useCache="false" resultType="com.zjq.pojo.User" parameterType="int">select * from user where id=#{id}
</select>

这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁⽤⼆级缓存,直接从数 据库中获取。
在mapper的同⼀个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓 存,如果不执⾏刷新缓存会出现脏读。
设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则 不会刷新。使⽤缓存时如果⼿动修改数据库表中的查询数据会出现脏读。

<select id="selectUserByUserId" flushCache="true" useCache="false"
resultType="com.zjq.pojo.User" parameterType="int">select * from user where id=#{id}
</select>

⼀般下执⾏完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不⽤设置,默认即可。

⼆级缓存整合Redis

上⾯我们介绍了 mybatis⾃带的⼆级缓存,但是这个缓存是单服务器⼯作,⽆法实现分布式缓存。 那么
什么是分布式缓存呢?假设现在有两个服务器1和2,⽤户访问的时候访问了 1服务器,查询后的缓 存就
会放在1服务器上,假设现在有个⽤户访问的是2服务器,那么他在2服务器上就⽆法获取刚刚那个 缓
存,为了解决这个问题,就得找⼀个分布式的缓存,专⻔⽤来存储缓存数据的,这样不同的服务器要缓存数
据都往它那⾥存,取缓存数据也从它那⾥取,如下图所示:

如上图所示,在⼏个不同的服务器之间,我们使⽤第三⽅缓存框架,将缓存都放在这个第三⽅框架中,
然后⽆论有多少台服务器,我们都能从缓存中获取数据。
这⾥我们介绍mybatis与redis的整合。
刚刚提到过,mybatis提供了⼀个eache接⼝,如果要实现⾃⼰的缓存逻辑,实现cache接⼝开发即可。
mybati s本身默认实现了⼀个,但是这个缓存的实现⽆法实现分布式缓存,所以我们要⾃⼰来实现。
redis分布式缓存就可以,mybatis提供了⼀个针对cache接⼝的redis实现类,该类存在mybatis-redis包
中实现:

pom⽂件

<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version>
</dependency>

配置⽂件Mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zjq.mapper.IUserMapper"><cache type="org.mybatis.caches.redis.RedisCache" /><select id="findAll" resultType="com.zjq.pojo.User" useCache="true">select * from user</select>

redis.properties

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

测试

@Test
public void SecondLevelCache(){SqlSession sqlSession1 = sqlSessionFactory.openSession();SqlSession sqlSession2 = sqlSessionFactory.openSession();SqlSession sqlSession3 = sqlSessionFactory.openSession();IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);lUserMapper mapper2 = sqlSession2.getMapper(lUserMapper.class);lUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);User user1 = mapper1.findUserById(1);sqlSession1.close(); //清空⼀级缓存User user = new User();user.setId(1);user.setUsername("lisi");mapper3.updateUser(user);sqlSession3.commit();User user2 = mapper2.findUserById(1);System.out.println(user1==user2);
}

源码分析

RedisCache和⼤家普遍实现Mybatis的缓存⽅案⼤同⼩异,⽆⾮是实现Cache接⼝,并使⽤jedis操作缓存;不过该项⽬在设计细节上有⼀些区别;

public final class RedisCache implements Cache {public RedisCache(final String id) {if (id == null) {throw new IllegalArgumentException("Cache instances require anID");}this.id = id;RedisConfig redisConfig =RedisConfigurationBuilder.getInstance().parseConfiguration();pool = new JedisPool(redisConfig, redisConfig.getHost(),redisConfig.getPort(),redisConfig.getConnectionTimeout(),redisConfig.getSoTimeout(), redisConfig.getPassword(),redisConfig.getDatabase(), redisConfig.getClientName());}
}

RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的⽅式很简单,就是调⽤RedisCache的带有String参数的构造⽅法,即RedisCache(String id);⽽在RedisCache的构造⽅法中,调⽤了 RedisConfigu rationBuilder 来创建 RedisConfig 对象,并使⽤ RedisConfig 来创建JedisPool。RedisConfig类继承了JedisPoolConfig,并提供了 host,port等属性的包装,简单看⼀下RedisConfig的属性:

public class RedisConfig extends JedisPoolConfig {private String host = Protocol.DEFAULT_HOST;private int port = Protocol.DEFAULT_PORT;private int connectionTimeout = Protocol.DEFAULT_TIMEOUT;private int soTimeout = Protocol.DEFAULT_TIMEOUT;private String password;private int database = Protocol.DEFAULT_DATABASE;private String clientName;

RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要⽅法:

public RedisConfig parseConfiguration(ClassLoader classLoader) {Properties config = new Properties();InputStream input =classLoader.getResourceAsStream(redisPropertiesFilename);if (input != null) {try {config.load(input);} catch (IOException e) {throw new RuntimeException("An error occurred while reading classpath property '"+ redisPropertiesFilename+ "', see nested exceptions", e);} finally {try {input.close();} catch (IOException e) {// close quietly}}}RedisConfig jedisConfig = new RedisConfig();setConfigProperties(config, jedisConfig);return jedisConfig;
}

核⼼的⽅法就是parseConfiguration⽅法,该⽅法从classpath中读取⼀个redis.properties⽂件:

host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password=
database=0
clientName=

并将该配置⽂件中的内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使⽤RedisConfig类创建完成redisPool;在RedisCache中实现了⼀个简单的模板⽅法,⽤来操作Redis:

private Object execute(RedisCallback callback) {Jedis jedis = pool.getResource();try {return callback.doWithRedis(jedis);} finally {jedis.close();}
}

模板接⼝为RedisCallback,这个接⼝中就只需要实现了⼀个doWithRedis⽅法⽽已:

public interface RedisCallback {Object doWithRedis(Jedis jedis);
}

接下来看看Cache中最重要的两个⽅法:putObject和getObject,通过这两个⽅法来查看mybatis-redis储存数据的格式:

@Override
public void putObject(final Object key, final Object value) {execute(new RedisCallback() {@Overridepublic Object doWithRedis(Jedis jedis) {jedis.hset(id.toString().getBytes(), key.toString().getBytes(),SerializeUtil.serialize(value));return null;}});
}@Override
public Object getObject(final Object key) {return execute(new RedisCallback() {@Overridepublic Object doWithRedis(Jedis jedis) {return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(),key.toString().getBytes()));}});
}

可以很清楚的看到,mybatis-redis在存储数据的时候,是使⽤的hash结构,把cache的id作为这个hash的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash的field,需要缓存的内容直接使⽤SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责对象的序列化和反序列化;

本文内容到此结束了,
如有收获欢迎点赞

MyBatis缓存和二级缓存整合Redis相关推荐

  1. Mybatis | Mybatis 一级缓存、二级缓存、三级自定义缓存(Redis)

    Mybatis 一级缓存.二级缓存.三级自定义缓存Redis实现 一.Mybatis 缓存 二.一级缓存 SqlSession级别 1.如何开启一级缓存 2.什么时候清除缓存? 3.什么时候缓存失效? ...

  2. mybatis一级缓存、二级缓存以及集成EnCache、Redis,避免脏读

    参考书目:<mybatis从入门到精通> 刘增辉  著 作者GitHub:https://github.com/abel533/MyBatis-Spring-Boot 一级缓存 mybat ...

  3. MyBatis→优缺点、select延迟加载、接口式MyBatis编程、一级缓存、二级缓存、集成Redis自定义缓存、Log4j

    MyBatis优缺点 select延迟加载 接口式MyBatis编程 一级缓存 一级缓存原理 一级缓存命中原则 一级缓存销毁 一级缓存避免脏读不可重复读 一级缓存与spring@事务 二级缓存 与一级 ...

  4. Mybatis一级缓存,二级缓存的实现就是这么简单

    介绍 又到了一年面试季,所以打算写一点面试常问的东西,争取说的通俗易懂.面试高级岗,如果你说熟悉Mybatis,下面这些问题基本上都会问 Mybatis插件的实现原理? 如何写一个分页插件? Myba ...

  5. mybatis中一级缓存和二级缓存

    1.一级缓存 一级缓存是SqlSession级别的缓存.在操作数据库时需要构造 sqlSession对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据.不同的sqlSessio ...

  6. 研究一天,终于把MyBatis的一级缓存和二级缓存搞清楚了

    介绍 MyBatis 现在是面试必问的,其中最主要的除了一些启动流程,基础语法,那么就是缓存问题了. 大家都知道 MyBatis 是有二级缓存,底层都是用HashMap实现的,key为CacheKey ...

  7. 【MyBatis学习13】MyBatis中的二级缓存

    1. 二级缓存的原理 前面介绍了,mybatis中的二级缓存是mapper级别的缓存,值得注意的是,不同的mapper都有一个二级缓存,也就是说,不同的mapper之间的二级缓存是互不影响的.为了更加 ...

  8. 项目整合一级缓存和二级缓存

    Redis+ehCache实现两级缓存 spring boot中集成了spring cache,并有多种缓存方式的实现,如:Redis.Caffeine.JCache.EhCache等等.但如果只用一 ...

  9. MyBatis】MyBatis一级缓存和二级缓存

    转载自  MyBatis]MyBatis一级缓存和二级缓存 MyBatis自带的缓存有一级缓存和二级缓存 一级缓存 Mybatis的一级缓存是指Session缓存.一级缓存的作用域默认是一个SqlSe ...

最新文章

  1. 机器学习中的三对性能度量参数
  2. Java Web知识梳理
  3. DBMS_STATS.GATHER_TABLE_STATS详解
  4. LC.exe”已退出,代码为 -1
  5. 3.1、Eclipse
  6. asp.net core根据用户权限控制页面元素的显示
  7. Windows下FFmpeg各版本库文件下载
  8. 嵌入式应用程序下载到ARM开发板后如何运行程序?
  9. android 微信文件存储,安卓微信文件存储位置
  10. 清华师哥封神之作!RocketMQ核心笔记疯传Ali内网
  11. gradle系列-1-gradle -x test clean build
  12. Spring-IoC注解
  13. 概率论与数理统计 基本概念
  14. EFR32 资源汇总
  15. 使用ExcelJs导出表格设置样式、添加边框
  16. VC的静态链接库 动态链接库
  17. 快速掌握SAP BDC数据导入
  18. 速营社团队分享互联网发展过程那些疯狂的时代
  19. 信息学竞赛中可能用到的初等数论
  20. 为搬砖做一点准备工作

热门文章

  1. 【每日一题】打卡 15
  2. 基于MATLAB的数字滤波器语音信号去噪
  3. macOS Final Cut Pro X 视频剪切 基本操作
  4. Js参数RSA加密传输之jsencrypt.js的使用
  5. 两台电脑之间Sql Server数据库的链接配置
  6. c语言求ln1加到lnn的平方根,ln函数的运算法则
  7. Android平板电脑使用评测
  8. Tencent_机器翻译_文本翻译
  9. Android 夜间模式实现
  10. 北理计算机学院高扬,北理工MBA戚高扬备考故事:| 为梦起航 圆梦北理