1、缓存

1.1 缓存背景

在系统性能测试3.3.2这一节中,已经发现了频繁读写数据库,非常影响整个系统的性能指标,比如系统的TPS,QPS,吞吐量等等都很低。为了提高系统整体性能,在有限的硬件条件下,尽量能够让更多的用户同时访问微服务,而且还有较高的稳定性,就要尽量避免频繁读写数据库。解决办法就是把常用的数据放入缓存中,这样web请求过来,就可直接从缓存中给数据,不用频繁查询数据库了,从而达到整体系统性能提升。

1.2 缓存的使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作。

1.2.1 哪些数据适合放入缓存

(1)即时性、数据一致性要求不高的数据

(2)访问量大且更新频率不高的数据(读多,写少)

举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率 来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。

1.2.2 缓存一般使用流程

 注意:在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没 有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致 问题

2、缓存基本功—Redis

现在主流承担缓存职能的是Redis,是一种非关系型数据库(NoSQL),比起关系型数据库(比如Mysql,Oracle)的优点就是只用键值对存储,读写,查询效率都很高。缺点自然就是关系型数据库能处理的字段关系,NoSQL又不好处理。总之,知道Redis适合做缓存就对了,至于非关系数据库和Redis想深入了解的,请阅读

2.1 简介

Tip:本文介绍和缓存相关的Redis的使用,想深入了解非关系型数据库(NoSQL)以及Redis,请阅读https://blog.csdn.net/u011863024/article/details/107476187,写得非常非常全面。

2.2 Redis五大数据类型及常用命令

以上只是大纲,具体细化,还得花时间挨个百度。

2.3 jedis

Redis的java客户端,java程序员通过jedis来操作Redis数据库。了解一下即可,主要操作的就是下面注意事项中的依赖替换,jedis的操作都封装在springcache里了,至少在缓存方面,我们也没啥机会用这个jedis

特别注意:

springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信,效率确实比Jedis更好,但是有bug会导致netty堆外内存溢出,暂时没有更好的解决办法(其实可能已经解决了,我学这个的时候,老师还用的spring2.1.8,现在都spring5.x了),因此只能Lettuce新版本看能不能解决此bug,或者切换使用jedis。

我们要操作的就是从springboot redis的starter中排除掉lettuce,然后补上jedis依赖,其他操作交给后面的SpringCache框架。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions>
</dependency>
<!-- 替代上面被排除的lettuce-->
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>

3、缓存失效问题

本节讲的缓存失效问题,是你自己去写一个缓存,需要避免的问题。缓存框架springcache都已经基本解决了这些问题(说基本是因为还有一类复杂问题没有解决),我们只需要配置springcache即可,但是首先还是需要了解下缓存失效问题。

3.1 缓存穿透

(1)简介

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数 据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层(Mysql)去查询,失去了缓存的意义。

(2)风险

黑客利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃

(3)解决办法

解决: 把空结果null也加入缓存、并且设置短的过期时间。(代码先不慌,后文springcache一个注解就解决)

3.2 缓存雪崩

缓存雪崩是指在我们设置缓存时key采用了相同的过期时间, 导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重导致雪崩雪崩

  • 解决办法:

原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件(代码先不慌,后文springcache一个注解就解决)

3.3 缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,成为非常“热点”的数据,比如“加拿大电鳗”,突然就变热点了。

如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

  • 解决办法:

加锁:大量并发只让一个去查,其他人等待,查到以后释放锁,其他 人获取到锁,先查缓存,就会有数据,不用去db。(代码先不慌,后文springcache一个注解就解决)

4、分布式锁框架 Redisson

分布式锁,和普通单体应用的锁用法都一样的,只是分布式锁用Redisson就是了

官方文档:目录 · redisson/redisson Wiki · GitHub

官方文档非常不友好,打开就一堆锁,全部学完是不可能的。我这里就介绍项目中,最常用的读写锁,

4.1 Redisson的使用

4.1.1 引入依赖

<!-- 以后使用redisson作为所有分布式锁,分布式对象等功能框架-->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.12.0</version>
</dependency>

4.1.2 配置Redisson

就是创建一个RedissonCliet,加入Spring容器

@Configuration
public class MyRedissonConfig {/*** 所有对 Redisson 的使用都是通过 RedissonClient** @return* @throws IOException*/@Bean(destroyMethod = "shutdown")public RedissonClient redisson() throws IOException {// 1、创建配置Config config = new Config();// Redis url should start with redis:// or rediss://config.useSingleServer().setAddress("redis://127.0.0.1:6379");// 2、根据 Config 创建出 RedissonClient 实例return Redisson.create(config);}}

更多配置方法:参考官网https://github.com/redisson/redisson/wiki,的程序化方式(代码)配置,可自行研究配置文件方式配置

4.2 读写锁

Redisson框架提供了  writeValue() 和 readValue()方法  来加读写锁

为了达成以下效果,读写锁是一把锁“rw-lock"
    读写锁:确保正在更新数据的时候,读不到旧数据,一直等待直到数据更新完成。 有点像mysql的禁止不可重复读
    读写锁最终效果:一定能读到最新数据

写锁:排它锁(互斥锁,独享锁),不准其他线程同时进来修改数据,只要写锁没释放,其他线程就必须一直等待
    读锁:共享锁,其他线程可以一起读,但是不准修改

一个线程在调用读方法readValue(),同时另一个线程也在调用读方法readValue():web浏览器中,两个页面同时访问xxx/read
    读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功

同理 一个线程在调用写方法writeValue(),同时另一个线程调用读方法readValue():web浏览器中,一个页面访问xxx/write,另一个访问xxx/read
    写 + 读 :必须等待写锁释放

写 + 写 :阻塞方式
    读 + 写 :有读锁。写也需要等待

规律:只要有写的存在,就必须等待

//写锁@GetMapping(value = "/write")@ResponseBodypublic String writeValue() {RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");String s = "";RLock rLock = readWriteLock.writeLock();try {//1、改数据加写锁rLock.lock();s = UUID.randomUUID().toString();ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();ops.set("writeValue",s); //给redis存入数据TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();} finally {rLock.unlock(); //释放写锁,然后readValue()方法,才可以操作(读写方法用的同一把锁)}return s;}//读锁@GetMapping(value = "/read")@ResponseBodypublic String readValue() {String s = "";RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");//加读锁,拿到锁加锁成功后再执行业务代码RLock rLock = readWriteLock.readLock();try {rLock.lock();ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();s = ops.get("writeValue");try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }} catch (Exception e) {e.printStackTrace();} finally {rLock.unlock();}return s;}

5、Spring Cache(重点,项目中就是用它来缓存了)

5.1 简介

SpringCache的作用就是将从DB(一般指Mysql)查到的数据,添加进缓存(一般用Redis)。

5.2 使用方法

5.2.1 spring配置文件中

配置文件中,主要完成个性化定制功能

#本项目使用的是redis来缓存
spring.cache.type=redis#设置缓存过期时间TTL,单位为毫秒,3600000就是1个小时
spring.cache.redis.time-to-live=3600000#一般规则就是,开启前缀,但是不指定缓存前缀,就用分区名作为前缀
#开启使用前缀,默认也是开启。只有false的时候,才需要写配置
#比如本项目的redis中,存储的名字就是db0/category下的 category::getCatalogJson
spring.cache.redis.use-key-prefix=true
#如果指定了前缀就用我们指定的前缀(给redis的key的前缀),如果没有指定,就默认使用缓存的名字(分区名字)作为前缀(@Cacheable注解的value属性)。
#spring.cache.redis.key-prefix=CACHE_#是否缓存空值,防止缓存穿透,一定要用
spring.cache.redis.cache-null-values=true

5.2.2 标注解

(1)启动类注解(标记在配置类或主启动类上,一般放在配置类上)

① @EnableConfigurationProperties(CacheProperties.class) :主要用于将SpringCache的默认存储数据类型修改后(一般都要改成Json),让spring配置文件里的配置生效(看后面案例,也很难文字解释)

② @EnableCaching: 开启使用缓存

@EnableConfigurationProperties(CacheProperties.class) //让application.yml中的缓存相关配置生效
@EnableCaching //开启使用缓存
@Configuration//这个是spring注解,跟缓存无关
public class MyCacheConfig {/*** 配置文件的配置没有用上* 1. 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache")*                                public class CacheProperties* <p>* 2. 要让他生效,要加上 @EnableConfigurationProperties(CacheProperties.class)*/@Bean //返回一个RedisCacheConfiguration 加入spring容器,即可完成配置public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
//....
}

(2)业务类注解(一般标记在方法上)

方法上主要就是完成业务功能。即对缓存Redis的增、删、改、查

① @Cacheable:将当前方法的返回值保存到缓存中;(如果缓存中有该结果,就不调用该方法) 逻辑:查找缓存 - 有就返回 -没有就执行方法体 - 将结果缓存起来;
② @CachePut :和@Cacheable一样,都是将方法的返回值添加缓存。但差异是,要先执行方法体。有结果就缓存,没有就算了 逻辑:执行方法体 - 将结果缓存起来
总结: @Cacheable 适用于查询数据的方法,@CachePut 适用于更新数据的方法。

③ @CacheEvict  : 触发将数据从缓存删除的操作,但是只删除一个或者所有;
④ @Cacheing:组合以上多个操作,比如可以弥补@CacheEvict只能删除一个的不足,删除多个,就用这个注解组合,详情见后面的案例;
⑤ @CacheConfig:在类级别共享缓存的相同配置

5.3 SpringCache使用案例

5.3.1 引入依赖(SpringCache+Redis)

<!--我这个springboot项目,父pom已经版本仲裁过了,所以不需要写版本号-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions>
</dependency>
<!--替代上面被排除的lettuce-->
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>

5.3.2 配置类

配置类主要是有两个任务

(1)基本任务:配置创建缓存的Bean组件,加入spring容器内

(2)进阶任务

springCache将DB中的数据存入Redis,默认是存的值是java序列化后的格式。为了能更好的跨语言,跨平台,我们需要通过配置让SpringCache向Redis中存的值是Json。

但是修改默认配置以后,又会导致SpringCache没法读取到spring配置文件application.propertis,因此在修改默认配置的同时,还要解决这个问题。

/*** @author: Taoji* @create: 2021-08-5 15:19* 单独写配置: 配置给redis中存放json,默认是存放java序列化的一堆看不懂的乱码,不适合跨语言,跨平台使用** 配置原理*   CacheAutoConfiguration -> RedisCacheConfiguration -> 自动配置了RedisCacheManager*   ->初始化所有的缓存 -> 每个缓存决定使用什么配置*   如果redisCacheConfiguration有自定义配置,就使用自定义配置 (下面的配置类,就是配这个)*   如果没有自定义配置,才会使用默认配置*   想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可,就会应用到当前的RedisCacheManager管理的所有缓存分区中**/@EnableConfigurationProperties(CacheProperties.class) //让application.yml中的缓存相关配置生效
@Configuration
@EnableCaching
public class MyCacheConfig {/*** 配置文件的配置没有用上* 1. 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache")*                                public class CacheProperties* <p>* 2. 要让他生效,要加上 @EnableConfigurationProperties(CacheProperties.class)*/@Bean //返回一个RedisCacheConfiguration 加入spring容器,即可完成配置public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();// config = config.entryTtl();config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));/**只写以上代码,配置文件不会生效的(我们在spring配置文件中写的ttl时间就不生效)* 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache")*                                 public class CacheProperties* 现在要让配置文件中所有的配置都生效要加上 @EnableConfigurationProperties(CacheProperties.class)同时加入以下代码:*/CacheProperties.Redis redisProperties = cacheProperties.getRedis();//如果配置文件不为空,就去配置文件中获取数据if (redisProperties.getTimeToLive() != null) {config = config.entryTtl(redisProperties.getTimeToLive());}if (redisProperties.getKeyPrefix() != null) {config = config.prefixKeysWith(redisProperties.getKeyPrefix());}if (!redisProperties.isCacheNullValues()) {config = config.disableCachingNullValues();}if (!redisProperties.isUseKeyPrefix()) {config = config.disableKeyPrefix();}return config;}}

5.3.3 改配置文件,application.properties

特别注意:

设置缓存过期时间,就是在解决3.2中的缓存雪崩

设置缓存空值,就是在解决3.1中的缓存穿透

(不要慌,缓存击穿问题的解决方法在5.3.4中)

#本项目使用的是redis来缓存
spring.cache.type=redis#设置缓存过期时间TTL,单位为毫秒,3600000就是1个小时
spring.cache.redis.time-to-live=3600000#一般规则就是,开启前缀,但是不指定缓存前缀,就用分区名作为前缀
#开启使用前缀,默认也是开启。只有false的时候,才需要写配置
spring.cache.redis.use-key-prefix=true
#如果指定了前缀就用我们指定的前缀(给redis的key的前缀),如果没有指定,就默认使用缓存的名字(分区名字)作为前缀(@Cacheable注解的value属性)。
#spring.cache.redis.key-prefix=CACHE_#是否缓存空值,防止缓存穿透,一定要用
spring.cache.redis.cache-null-values=true

小细节:spring.cache.redis.use-key-prefix=true ,允许使用前缀,但是不指定前缀,这样缓存的键就会统一用@Cacheable(value="xx")中的值xx作为前缀,这样数据在redis中更规范

5.3.4 编写业务逻辑

(1)向缓存Redis添加数据

方法做的事情就是从Mysql查询数据,并封装成javaBean。由于有了@Cacheable注解,自然就存到缓存Redis中去了,存在redis的category目录下,key就是方法名,getLevel1Categories,由于前面5.3.3配置文件中配置了使用用前缀,又未指定前缀,因此默认前缀就是category,因此该数据再redis中key为category::getLevel1Categories

特别注意:sync=true

给微服务加锁,防止多个相同为服务同时进来查询一个正好过期的数据,造成缓存击穿,解决3.3中的缓存击穿问题

/ @Cacheable:将当前方法的返回值保存到缓存中;(如果缓存中有该结果,IndexController再调用该方法,也不会执行)//value属性:缓存的分区(按照业务类型分)】,就是在redis下创建一个名为 category 文件夹//key属性:就是redis的key,用spel表达式取值,参考官网#root.method.name 代表用方法名getLevel1Categories作为 key//sync属性:给微服务加锁,防止多个相同为服务同时进来查询一个正好过期的数据,造成缓存击穿//特别注意:key中的是spel表达式,""里面的还是变量  如果写普通字符串,一定要“‘’”//找1级分类,1级分类就是pms_category数据表中 cat_level为1 或者 parent_cid为0 的项目就是1级分类@Cacheable(value = {"category"}, key = "#root.methodName", sync = true)@Overridepublic List<CategoryEntity> getLevel1Categories() {//去数据库查数据List<CategoryEntity> categoryEntities = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));return categoryEntities;}

(2)删除缓存中的多个过期值(由于是多个,所以不能只用@CacheEvict)

下面代码演示了,SpringCache对缓存数据的删除操作。

方法中的两句代码说明该方法从数据库中重新更新了数据,自然会导致缓存中的数据已经过期了,是错误的。又由于存在多个过期值,因此才用@Caching 组合多个@CacheEvict,将缓存中的过期值删了。

说明:不用担心,删除缓存后,下次前端请求没缓存了。下次请求进来会先查缓存,发现没缓存了,就会用(1)中的 @Cacheable添加好最新版缓存。

//@CacheEvict(value = "category",key = "'getLevel1Categories'") //只能删除一个key,或者所有key,删除多个key的话,用@Caching
//@CacheEvict(value = "category",allEntries = true) //删除category目录里(分区)的所有key
@Caching(evict = {  //就是组合使用 缓存的哪几种注解,比如@Cacheable,@CacheEvict,@CachePut,@CacheConfig等等。这里是删除多个key,就组合@CacheEvict即可@CacheEvict(value = "category",key = "'getLevel1Categories'"),@CacheEvict(value = "category",key = "getCatalogJson")})
@Transactional
@Override
public void updateCascade(CategoryEntity category) {this.updateById(category);categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());//修改过数据库以后,数据库和缓存中数据已经不一致了,就删除缓存中的数据   相关搜索失效模式//实践中,通过springcache的@CacheEvict即可达到以下代码的效果即删除缓存中的数据,完成失效模式}

缓存 redis 缓存失效 分布式锁 Redisson SpringCache相关推荐

  1. Redis分布式锁Redisson

    文章目录 分布式锁 不可重入Redis分布式锁 Redisson 快速入门 可重入的Redis分布式锁 Redisson的multiLock 分布式锁 分布式锁:满足分布式系统或集群模式下多进程可见并 ...

  2. 基于 Redis 实现的分布式锁

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:我的大学到研究生自学 Java 之路,过程艰辛,不放弃,保持热情,最终发现我是这样拿到大厂 offer 的! 作 ...

  3. Redlock——Redis集群分布式锁

    欢迎关注方志朋的博客,回复"666"获面试宝典 前言 分布式锁是一种非常有用的技术手段.实现高效的分布式锁有三个属性需要考虑: 安全属性:互斥,不管什么时候,只有一个客户端持有锁 ...

  4. Redis 学习笔记-NoSQL数据库 常用五大数据类型 Redis配置文件介绍 Redis的发布和订阅 Redis_事务_锁机制_秒杀 Redis应用问题解决 分布式锁

    1.NoSQL数据库 1.1 NoSQL数据库概述 NoSQL(NosQL = Not Only sQL ),意即"不仅仅是sQL",泛指非关系型的数据库.NoSQL不依赖业务逻辑 ...

  5. 【Redis笔记】一起学习Redis | 如何利用Redis实现一个分布式锁?

    一起学习Redis | 如何利用Redis实现一个分布式锁? 前提知识 什么是分布式锁? 为什么需要分布式锁? 分布式锁的5要素和三种实现方式 实现分布式锁 思考思考 基础方案 改进方案 保证setn ...

  6. 17、Redis、Zk分布式锁实现原理

    我们在编程有很多场景使用本地锁和分布式锁,但是是否考虑这些锁的原理是什么?本篇讨论下实现分布式锁的常见办法及他们实现原理. 一.使用锁的原则 使用本地锁和分布式锁是为了解决并发导致脏数据的场景,使用锁 ...

  7. 花5min就能搞清楚redis和zookeeper分布式锁的区别,太有必要读一下了

    今天有个师弟问到了我这个问题,我说网络上文章有很多,自己查一下吧,他说读了好几篇还是不太清楚,于是我就搜了一下,呃-- 最终还是耐心地给他上了一课,他听完以后感激涕零,想到他晚上回到家,倒上二两散装白 ...

  8. Redis cache-aside模型-分布式锁等问题研究

    目录 1.Read模式: 1.布隆过滤器:缓存穿透 2.并发排他 3.小总结: 2.Write模式:双写一致性 3.Redis分布式锁: 4.Redis缓存存什么数据: 参考文章: Cache-asi ...

  9. redis系列:分布式锁

    1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境, ...

  10. Redis 如何实现分布式锁?

    锁是多线程编程中的一个重要概念,它是保证多线程并发时顺利执行的关键.我们通常所说的"锁"是指程序中的锁,也就是单机锁,例如 Java 中的 Lock 和 ReadWriteLock ...

最新文章

  1. openshift scc解析
  2. 【译】用Fragment创建动态的界面布局(附Android示例代码)
  3. 【MM模块】 Goods Receipt 收货 4
  4. VTK:vtkCompositePolyDataMapper2用法实战
  5. mate 树莓派4b安装ubuntu_树莓派4B安装安装Ubuntu Mate 16.04
  6. php批量生成html文件,php 批量生成html、txt文件
  7. 防止表单重复提交的解决方案整理
  8. 【转】30种MySQL索引优化的方法
  9. python 验证码图片 模拟登录_Python 模拟生成动态产生验证码图片的方法
  10. 二分答案——进击的奶牛(洛谷 P1824)
  11. 详解ADSL接入方式的异同比较
  12. SQL Server中授予用户查看对象定义的权限
  13. Java连接数据库(学生管理系统案例,可以实现增删改查)
  14. 嵌入式常见 c语言笔试题
  15. 关于使用开源版urule决策引擎优化性能和配置客户端集群同步生效的问题
  16. Turtle库学习--TurtleScreen/Screen 方法及对应函数
  17. Zedboard(一)开发环境Vivado
  18. 科维的时间管理法—《可以量化的管…
  19. 用STM32F103完成对SD卡的数据读取
  20. 十六进制颜色值和ARGB颜色值的转换

热门文章

  1. Unity Shader appdata详解
  2. 执行npm install报错:npm ERR! code EINTEGRITY,npm ERR! 最彻底,最实用的方法就是更新node版本
  3. 鄙人最新作JS自动适应的图片弹窗
  4. 更改OneDrive网页版OneNote笔记使用桌面应用打开时的默认应用
  5. 希捷硬盘保修时间查询
  6. 电动48V/60V自行车/摩托车/观光车电池检测设备,满足GB38031新国标测试
  7. 什么是增量绩效管理?华为是如何做
  8. 区块链游戏——开发平台总览:EOSIO
  9. 计算机装系统找不到硬盘,安装系统找不到硬盘怎么办
  10. Vue问题之 项目目录结构介绍