文章概念描述


分布式缓存描述:
分布式缓存重点是在分布式上,相信大家接触过的分布式有很多中,像分布式开发,分布式部署,分布式锁、事物、系统 等有很多。使我们对分布式本身就有一个很明确的认识,分布式就是有多个应用程序组成,可能分布在不同的服务器上,最终都是在为web端提供服务。
分布式缓存有以下几点优点:

  1. 所有的Web服务器上的缓存数据都是相同的,不会因为应用程序不同,服务器的不同导致缓存数据的不一样。
  2. 缓存的是独立的不受Web服务器的重新启动或被删除添加的影响,也就是说这些Web的改变不到导致缓存数据的改变。

传统的单体应用架构因为用户的访问量的不高,缓存的存在大多数都是存储用户的信息,以及一些页面,大多数的操作都是直接和DB进行读写交互,这种架构简单,也称为简单架构,
传统的OA项目比如ERP,SCM,CRM等系统因为用户量不大也是因为大多数公司业务的原因,单体应用架构还是很常用的架构,但是有些系统随着用户量的增加,业务的扩张扩展,导致DB的瓶颈的出现。

以下我所了解到的关于这种情况的处理有以下两种

(1):当用户访问量不大,但是读写的数据量很大的时候,我们一般采取的是,对DB进行读写分离、一主多从、对硬件进行升级的方式来解决DB瓶颈的问题。
  这样的缺点也同样纯在:

1、用户量大的时候怎么办?,
2、对于性能的提升有限,
3、性价比不高。提升一点性能就需要花费很多代价,(打个比方,现在的I/O吞吐量是0.9的需要提升到1.0,我们在增加机器配置的情况下这个价格确实很可观的)

(2):当用户访问量也增加的时候,我们就需要引入缓存了来解决了,一张图描述缓存的大致的作用。

缓存主要针对的是不经常发生改变的并且访问量很大的数据,DB数据库可以理解为只作为数据固化的或者只用来读取经常发生改变的数据,上图中我没有画SET的操作,就是想特意说明一下,缓存的存在可以作为一个临时的数据库,我们可以通过定时的任务的方式去同步缓存和数据库中的数据,这样做的好处是可以转移数据库的压力到缓存中。

缓存的出现解决了数据库压力的问题,但是当以下情况发生的时候,缓存就不在起到作用了,缓存穿透、缓存击穿、缓存雪崩这三种情况

缓存穿透:我们的程序中用缓存的时候一般采取的是先去缓存中查询我们想要的缓存数据,如果缓存中不存在我们想要的数据的话,缓存就失去了做用(缓存失效)我们就是需要伸手向DB库去要数据,这个时候这种动作过多数据库就崩溃了,这种情况需要我们去预防了。比如说:我们向缓存获取一个用户信息,但是故意去输入一个缓存中不存在的用户信息,这样就避过了缓存,把压力重新转移到数据上面了。对于这种问题我们可以采取,把第一次访问的数据进行缓存,因为缓存查不到用户信息,数据库也查询不到用户信息,这个时候避免重复的访问我们把这个请求缓存起来,把压力重新转向缓存中,有人会有疑问了,当访问的参数有上万个都是不重复的参数并且都是可以躲避缓存的怎么办,我们同样把数据存起来设置一个较短过期时间清理缓存。

缓存击穿:事情是这样的,对于一些设置了过期时间的缓存KEY,在过期的时候,程序被高并发的访问了(缓存失效),这个时候使用互斥锁来解决问题,

互斥锁原理:通俗的描述就是,一万个用户访问了,但是只有一个用户可以拿到访问数据库的权限,当这个用户拿到这个权限之后重新创建缓存,这个时候剩下的访问者因为没有拿到权限,就原地等待着去访问缓存。

永不过期:有人就会想了,我不设置过期时间不就行了吗?可以,但是这样做也是有缺点的,我们需要定期的取更新缓存,这个时候缓存中的数据比较延迟。

缓存雪崩:是指多种缓存设置了同一时间过期,这个时候大批量的数据访问来了,(缓存失效)数据库DB的压力又上来了。解决方法在设置过期时间的时候在过期时间的基础上增加一个随机数尽可能的保证缓存不会大面积的同事失效。

项目准备


1、首先安装Redis,可以参考这里
2、然后下载安装:客户端工具:RedisDesktopManager(方便管理)
3、在我们的项目Nuget中 引用 Microsoft.Extensions.Caching.Redis

为此我们新建一个ASP.NET Core MVC项目,在项目Startup类的ConfigureServices方法中先注册Redis服务:

public void ConfigureServices(IServiceCollection services)
{
//将Redis分布式缓存服务添加到服务中
services.AddDistributedRedisCache(options =>
{
//用于连接Redis的配置 Configuration.GetConnectionString("RedisConnectionString")读取配置信息的串
options.Configuration = "localhost";// Configuration.GetConnectionString("RedisConnectionString");
//Redis实例名DemoInstance
options.InstanceName = "DemoInstance";
});
services.AddMvc();
}

也可以在上面注册Redis服务的时候,指定Redis服务器的IP地址、端口号和登录密码:

public void ConfigureServices(IServiceCollection services)
{
//将Redis分布式缓存服务添加到服务中
services.AddDistributedRedisCache(options =>
{
//用于连接Redis的配置 Configuration.GetConnectionString("RedisConnectionString")读取配置信息的串
options.Configuration = "192.168.1.105:6380,password=1qaz@WSX3edc$RFV";//指定Redis服务器的IP地址、端口号和登录密码
//Redis实例名DemoInstance
options.InstanceName = "DemoInstance";
});
services.AddMvc();
}

后面我们会解释上面options.InstanceName设置的Redis实例名DemoInstance是用来做什么的

此外还可以在services.AddDistributedRedisCache方法中指定Redis服务器的超时时间,如果调用后面介绍的IDistributedCache接口中的方法,对Redis服务器进行的操作超时了,会抛出RedisConnectionException和RedisTimeoutException异常,所以下面我们在注册Redis服务的时候,指定了三个超时时间:

public void ConfigureServices(IServiceCollection services)
{
//将Redis分布式缓存服务添加到服务中
services.AddDistributedRedisCache(options =>
{
options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions()
{
Password = "1qaz@WSX3edc$RFV",
ConnectTimeout = ,//设置建立连接到Redis服务器的超时时间为5000毫秒
SyncTimeout = ,//设置对Redis服务器进行同步操作的超时时间为5000毫秒
ResponseTimeout = //设置对Redis服务器进行操作的响应超时时间为5000毫秒
}; options.ConfigurationOptions.EndPoints.Add("192.168.1.105:6380");
options.InstanceName = "DemoInstance";
});
services.AddMvc();
}

其中ConnectTimeout是建立连接到Redis服务器的超时时间,而SyncTimeout和ResponseTimeout是对Redis服务器进行数据操作的超时时间。注意上面我们使用了options.ConfigurationOptions属性来设置Redis服务器的IP地址、端口号和登录密码。

如果你使用的Redis服务是在公网上的,那么强烈建议使用SSL安全加密来传输Redis数据,这样数据就是加密后在公网上传输的,更安全。

我们可以在Startup类的ConfigureServices方法中配置使用SSL来传输Redis数据。

我们可以通过Redis连接字符串来配置SSL:

public void ConfigureServices(IServiceCollection services)
{
//将Redis分布式缓存服务添加到服务中
services.AddDistributedRedisCache(options =>
{
//用于连接Redis的配置 Configuration.GetConnectionString("RedisConnectionString")读取配置信息的串
options.Configuration = "192.168.1.105:6380,password=1qaz@WSX3edc$RFV,ssl=True,abortConnect=False";
//指定Redis服务器的IP地址、端口号和登录密码,并且启用SSL安全加密传输Redis数据 //Redis实例名DemoInstance
options.InstanceName = "DemoInstance";
}); services.AddMvc();
}

注意连接字符串中的"ssl=True"就是设置Redis启用SSL安全加密传输数据。

我们也可以通过options.ConfigurationOptions属性来配置SSL:

public void ConfigureServices(IServiceCollection services)
{
//将Redis分布式缓存服务添加到服务中
services.AddDistributedRedisCache(options =>
{
options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions()
{
Password = "1qaz@WSX3edc$RFV",
ConnectTimeout = ,//设置建立连接到Redis服务器的超时时间为5000毫秒
SyncTimeout = ,//设置对Redis服务器进行同步操作的超时时间为5000毫秒
ResponseTimeout = ,//设置对Redis服务器进行操作的响应超时时间为5000毫秒
Ssl = true//设置启用SSL安全加密传输Redis数据
//SslProtocols = System.Security.Authentication.SslProtocols.Tls//还可以通过SslProtocols属性指定SSL具体用到的是什么协议,不过这个属性不是必须的
}; options.ConfigurationOptions.EndPoints.Add("192.168.1.105:6380");
options.InstanceName = "DemoInstance";
}); services.AddMvc();
}

这次我们是通过options.ConfigurationOptions属性,设置ConfigurationOptions类的Ssl属性为true,来启用SSL安全加密来传输Redis数据。

IDistributedCache 接口


在项目中引用:using Microsoft.Extensions.Caching.Distributed; 使用IDistributedCache

IDistributedCache接口包含同步和异步方法。 接口允许在分布式缓存实现中添加、检索和删除项。 IDistributedCache接口包含以下方法:
Get、 GetAsync
采用字符串键并以byte[]形式检索缓存项(如果在缓存中找到)。如果传入Get方法的键在Redis中不存在,那么Get方法会返回null。
Set、SetAsync
使用字符串键向缓存添加或更改项(byte[]形式)。
Refresh、RefreshAsync
根据键刷新缓存中的项,并重置其滑动过期超时值(如果有)。如果传入Refresh方法的键在Redis中不存在,Refresh方法不会报错,只是什么都不会发生而已,但是如果传入Refresh方法的参数为null,则会抛出异常。
Remove、RemoveAsync
根据键删除缓存项。如果传入Remove方法的键在Redis中不存在,Remove方法不会报错,只是什么都不会发生而已,但是如果传入Remove方法的参数为null,则会抛出异常。

如上所述,由于IDistributedCache接口的Set和Get方法,是通过byte[]字节数组来向Redis存取数据的,所以从某种意义上来说不是很方便,下面我封装了一个RedisCache类,可以向Redis中存取任何类型的数据。

其中用到了Json.NET Nuget包,来做Json格式的序列化和反序列化:

using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using System.Text; namespace AspNetCoreRedis.Assembly
{
/// <summary>
/// RedisCache缓存操作类
/// </summary>
public class RedisCache
{
protected IDistributedCache cache; /// <summary>
/// 通过IDistributedCache来构造RedisCache缓存操作类
/// </summary>
/// <param name="cache">IDistributedCache对象</param>
public RedisCache(IDistributedCache cache)
{
this.cache = cache;
} /// <summary>
/// 添加或更改Redis的键值,并设置缓存的过期策略
/// </summary>
/// <param name="key">缓存键</param>
/// <param name="value">缓存值</param>
/// <param name="distributedCacheEntryOptions">设置Redis缓存的过期策略,可以用其设置缓存的绝对过期时间(AbsoluteExpiration或AbsoluteExpirationRelativeToNow),也可以设置缓存的滑动过期时间(SlidingExpiration)</param>
public void Set(string key, object value, DistributedCacheEntryOptions distributedCacheEntryOptions)
{
//通过Json.NET序列化缓存对象为Json字符串
//调用JsonConvert.SerializeObject方法时,设置ReferenceLoopHandling属性为ReferenceLoopHandling.Ignore,来避免Json.NET序列化对象时,因为对象的循环引用而抛出异常
//设置TypeNameHandling属性为TypeNameHandling.All,这样Json.NET序列化对象后的Json字符串中,会包含序列化的类型,这样可以保证Json.NET在反序列化对象时,去读取Json字符串中的序列化类型,从而得到和序列化时相同的对象类型
var stringObject = JsonConvert.SerializeObject(value, new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.All
}); var bytesObject = Encoding.UTF8.GetBytes(stringObject);//将Json字符串通过UTF-8编码,序列化为字节数组 cache.Set(key, bytesObject, distributedCacheEntryOptions);//将字节数组存入Redis
Refresh(key);//刷新Redis
} /// <summary>
/// 查询键值是否在Redis中存在
/// </summary>
/// <param name="key">缓存键</param>
/// <returns>true:存在,false:不存在</returns>
public bool Exist(string key)
{
var bytesObject = cache.Get(key);//从Redis中获取键值key的字节数组,如果没获取到,那么会返回null if (bytesObject == null)
{
return false;
} return true;
} /// <summary>
/// 从Redis中获取键值
/// </summary>
/// <typeparam name="T">缓存的类型</typeparam>
/// <param name="key">缓存键</param>
/// <param name="isExisted">是否获取到键值,true:获取到了,false:键值不存在</param>
/// <returns>缓存的对象</returns>
public T Get<T>(string key, out bool isExisted)
{
var bytesObject = cache.Get(key);//从Redis中获取键值key的字节数组,如果没获取到,那么会返回null if (bytesObject == null)
{
isExisted = false;
return default(T);
} var stringObject = Encoding.UTF8.GetString(bytesObject);//通过UTF-8编码,将字节数组反序列化为Json字符串 isExisted = true; //通过Json.NET反序列化Json字符串为对象
//调用JsonConvert.DeserializeObject方法时,也设置TypeNameHandling属性为TypeNameHandling.All,这样可以保证Json.NET在反序列化对象时,去读取Json字符串中的序列化类型,从而得到和序列化时相同的对象类型
return JsonConvert.DeserializeObject<T>(stringObject, new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
});
} /// <summary>
/// 从Redis中删除键值,如果键值在Redis中不存在,该方法不会报错,只是什么都不会发生
/// </summary>
/// <param name="key">缓存键</param>
public void Remove(string key)
{
cache.Remove(key);//如果键值在Redis中不存在,IDistributedCache.Remove方法不会报错,但是如果传入的参数key为null,则会抛出异常
} /// <summary>
/// 从Redis中刷新键值
/// </summary>
/// <param name="key">缓存键</param>
public void Refresh(string key)
{
cache.Refresh(key);
}
}
}

使用测试


然后我们在ASP.NET Core MVC项目中,新建一个CacheController,然后在其Index方法中来测试RedisCache类的相关方法:

public class CacheController : Controller
{
protected RedisCache redisCache; //由于我们前面在Startup类的ConfigureServices方法中调用了services.AddDistributedRedisCache来注册Redis服务,所以ASP.NET Core MVC会自动依赖注入下面的IDistributedCache cache参数
public CacheController(IDistributedCache cache)
{
redisCache = new RedisCache(cache);
} public IActionResult Index()
{
bool isExisted;
isExisted = redisCache.Exist("abc");//查询键值"abc"是否存在
redisCache.Remove("abc");//删除不存在的键值"abc",不会报错 string key = "Key01";//定义缓存键"Key01"
string value = "This is a demo key !";//定义缓存值 redisCache.Set(key, value, new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes()
});//设置键值"Key01"到Redis,使用绝对过期时间,AbsoluteExpirationRelativeToNow设置为当前系统时间10分钟后过期 //也可以通过AbsoluteExpiration属性来设置绝对过期时间为一个具体的DateTimeOffset时间点
//redisCache.Set(key, value, new DistributedCacheEntryOptions()
//{
// AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(10)
//});//设置键值"Key01"到Redis,使用绝对过期时间,AbsoluteExpiration设置为当前系统时间10分钟后过期 var getVaue = redisCache.Get<string>(key, out isExisted);//从Redis获取键值"Key01",可以看到getVaue的值为"This is a demo key !" value = "This is a demo key again !";//更改缓存值 redisCache.Set(key, value, new DistributedCacheEntryOptions()
{
SlidingExpiration = TimeSpan.FromMinutes()
});//将更改后的键值"Key01"再次缓存到Redis,这次使用滑动过期时间,SlidingExpiration设置为10分钟 getVaue = redisCache.Get<string>(key, out isExisted);//再次从Redis获取键值"Key01",可以看到getVaue的值为"This is a demo key again !" //下面我们来演示如何将键值"Key01"在Redis中设置为永不过期
redisCache.Remove(key);//首先要从Redis中删除键值"Key01",因为.NET Core无法将一个有绝对过期时间或滑动过期时间的键值在Redis中设置为永不过期,我们只能将键值"Key01"先从Redis中删除,然后重新添加键值"Key01"到Redis redisCache.Set(key, value, new DistributedCacheEntryOptions()
{
AbsoluteExpiration = null,
AbsoluteExpirationRelativeToNow = null,
SlidingExpiration = null
});//在添加键值"Key01"到Redis时,只要将DistributedCacheEntryOptions类的AbsoluteExpiration、AbsoluteExpirationRelativeToNow、SlidingExpiration三个属性都设置为null,那么键值"Key01"在Redis中就没有过期时间,也就是永不过期。
//这时可以在redis-cli中,使用指令:"ttl <键名称>",查看到键值"Key01"的剩余时间为-1,表示没有过期时间,永不过期。 redisCache.Set(key, value, new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds()
});//虽然.NET Core无法将Redis中有绝对过期时间或滑动过期时间的键值设置为永不过期,但是却可以将一个永不过期的键值设置为有绝对过期时间或滑动过期时间,取消永不过期,只需调用Set方法,将DistributedCacheEntryOptions类的AbsoluteExpiration、AbsoluteExpirationRelativeToNow、SlidingExpiration三个属性之一赋值即可。 redisCache.Remove(key);//从Redis中删除键值"Key01" return View();
}
}

我们可以使用"ttl"指令在redis-cli中,查看键值在Redis中还有多久过期,格式如下:

ttl <键名称>

该指令会返回一个整数,表示键值还有多少秒到过期时间,此外"ttl"指令还会返回两个负数,表示:

  • -1, 表示键值没有过期时间,即该键值在Redis中永不过期。
  • -2, 表示键值在Redis中不存在。

"ttl"指令的运行效果如下所示:

前面我们在项目的Startup类ConfigureServices方法中,调用services.AddDistributedRedisCache注册Redis服务的时候,有设置options.InstanceName = "DemoInstance",那么这个InstanceName到底有什么用呢?

当我们在上面的CacheController中调用Index方法的下面代码后:

string key = "Key01";//定义缓存键"Key01"
string value = "This is a demo key !";//定义缓存值 redisCache.Set(key, value, new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes()
});//设置键值"Key01"到Redis,使用绝对过期时间,AbsoluteExpirationRelativeToNow设置为当前系统时间10分钟后过期

我们使用redis-cli登录到Redis服务器中,使用Keys *指令查看当前Redis服务中存储的所有键时,可以看到结果如下:

可以看到虽然我们代码中向Redis存入的键是"Key01",但是实际上在Redis服务中存储的键是"DemoInstanceKey01",所以实际上真正存入Redis服务中的键是“InstanceName+键”这种组合键,因此我们可以通过设置不同的InstanceName来为不同的Application在Redis中做数据隔离,这就是InstanceName的作用

原文链接

在AspNetCore 中 使用Redis实现分布式缓存 (转载)的更多相关文章

  1. 在AspNetCore 中 使用Redis实现分布式缓存

    AspNetCore 使用Redis实现分布式缓存 上一篇讲到了,Core的内置缓存:IMemoryCache,以及缓存的基础概念.本篇会进行一些概念上的补充. 本篇我们记录的内容是怎么在Core中使 ...

  2. 【转载】在AspNetCore 中 使用Redis实现分布式缓存

    原文地址:https://www.cnblogs.com/szlblog/p/9045209.html AspNetCore 使用Redis实现分布式缓存 上一篇讲到了,Core的内置缓存:IMemo ...

  3. AspNetCore Redis实现分布式缓存

    分布式缓存描述: 分布式缓存重点是在分布式上,相信大家接触过的分布式有很多中,像分布式开发,分布式部署,分布式锁.事物.系统 等有很多.使我们对分布式本身就有一个很明确的认识,分布式就是有多个应用程序 ...

  4. 三点须知:当我们在开发过程中需要用到分布式缓存Redis的时候

    当我们在开发过程中需要用到分布式缓存Redis的时候,我们首先要明白缓存在系统中用来做什么? 1. 少量数据存储,高速读写访问.通过数据全部in-momery 的方式来保证高速访问,同时提供数据落地的 ...

  5. ASP.Net Core 使用Redis实现分布式缓存

    本篇我们记录的内容是怎么在Core中使用Redis 和 SQL Server 实现分布式缓存. 一.文章概念描述   分布式缓存描述: 分布式缓存重点是在分布式上,相信大家接触过的分布式有很多中,像分 ...

  6. ASP.NET Core 使用 Redis 实现分布式缓存:Docker、IDistributedCache、StackExchangeRedis

    ASP.NET Core 使用 Redis 实现分布式缓存:Docker.IDistributedCache.StackExchangeRedis 前提:一台 Linux 服务器.已安装 Docker ...

  7. WEB 应用缓存解析以及使用 Redis 实现分布式缓存

    什么是缓存? 缓存就是数据交换的缓冲区,用于临时存储数据(使用频繁的数据).当用户请求数据时,首先在缓存中寻找,如果找到了则直接返回.如果找不到,则去数据库中查找.缓存的本质就是用空间换时间,牺牲数据 ...

  8. Redis实现分布式缓存

    Redis 分布式缓存实现(一) 1. 什么是缓存(Cache) 定义:就是计算机内存中的一段数据: 2. 内存中数据特点 a. 读写快    b. 断电立即丢失 3. 缓存解决了什么问题? a. 提 ...

  9. Redis-基本概念、java操作redis、springboot整合redis,分布式缓存,分布式session管理等

    NoSQL的引言 Redis数据库相关指令 Redis持久化相关机制 SpringBoot操作Redis Redis分布式缓存实现 Resis中主从复制架构和哨兵机制 Redis集群搭建 Redis实 ...

随机推荐

  1. Angular 6.X CLI(Angular.json) 属性详解

    Angular CLI(Angular.json) 属性详解 简介 angular cli 是angular commond line interface的缩写,意为angular的命令行接口.在an ...

  2. java中获取系统变量

    System.getProperty("java.version") 用来获取系统变量.getProperty()这个方法是获取指定键指示的系统属性的.以下是通过System.ge ...

  3. 1-1 sacc(scss)入门

    定义: CSS 预处理器定义了一种新的语言,其基本思想是,用一种专门的编程语言,为 CSS 增加了一些编程的特性,将 CSS 作为目标生成文件,然后开发者就只要使用这种语言进行编码工作. 通俗的说,“ ...

  4. 安卓开发_浅谈ListView(ArrayAdapter数组适配器)

    列表视图(ListView)以垂直的形式列出需要显示的列表项. 实现过程:新建适配器->添加数据源到适配器->视图加载适配器 在安卓中,有两种方法可以在屏幕中添加列表视图 1.直接用Lis ...

  5. 有关 Android Studio 重复引入包的问题和解决方案

    虽然相同包名相同类名的文件在不同 SDK 中出现的概率极低,但是一旦出现,处理起来就比较棘手.最好的解决方案就是联系提供 SDK 的技术人员反映问题,让其通过修改源码重新打包一个新的 Jar 包. 还 ...

  6. 微信小程序获取Access_token和页面URL生成小程序码或二维码

    1.微信小程序获取Access_token: access_token具体时效看官方文档. using System; using System.Collections.Generic; using ...

  7. 简单的分页小demo

    public class Demo { public static void main(String[] args) { Scanner sc = new Scanner(System.in); Sy ...

  8. 机器学习实战(Machine Learning in Action)学习笔记————07.使用Apriori算法进行关联分析

    机器学习实战(Machine Learning in Action)学习笔记————07.使用Apriori算法进行关联分析 关键字:Apriori.关联规则挖掘.频繁项集作者:米仓山下时间:2018 ...

  9. OneAPM大讲堂 | 监控数据的可视化分析神器 Grafana 的告警实践

    文章系国内领先的 ITOM 管理平台供应商 OneAPM 编译呈现. 概览 Grafana 是一个开源的监控数据分析和可视化套件.最常用于对基础设施和应用数据分析的时间序列数据进行可视化分析,也可以用 ...

  10. ADB三个进阶使用

    adb通过Wi-Fi连接手机 背景知识 Android系统底层运行着一个服务(adbd),也就是在手机系统内部,用于响应.管理大家在电脑端的adb命令连接,这个服务在启动时候会根据手机的配置监听USB ...