非关系型数据库有关的知识,学习后端必备。
Not Only SQL,
关系型数据库以表格
的形式存在,以行和列
的形式存取数据,关系型数据库这一系列的行和列被称为表,无数张表组成了数据库
,
关系型数据库能够支持复杂的 SQL 查询,能够体现出数据之间、表之间的关联关系;关系型数据库也支持事务,便于提交或者回滚。
常见的关系型数据库有 Oracle、DB2、Microsoft SQL Server、MySQL等。
非关系型数据库(感觉翻译不是很准确)称为 NoSQL
,也就是 Not Only SQL,不仅仅是 SQL。
非关系型数据库不需要写一些复杂的 SQL 语句,其内部存储方式是以 key-value
的形式存在可以把它想象成电话本的形式,每个人名(key)对应电话(value)。
非关系型数据库不需要经过 SQL 的重重解析,所以性能很高;非关系型数据库的可扩展性比较强,数据之间没有耦合性,遇见需要新加字段的需求,就直接增加一个 key-value 键值对即可。
常见的非关系型数据库主要有 Hbase、Redis、MongoDB 等。
如果您是应用程序开发人员,则可能在使用关系数据库管理系统 (RDBMS) 和结构化查询语言 (SQL) 方面有一些经验。在您开始使用 Amazon DynamoDB 时,您既会遇到许多相似之处,也会遇到许多不同之处。NoSQL 是一个术语,用于描述高度可用的、可扩展的并且已针对高性能进行优化的非关系数据库系统。有别于关系模型,NoSQL 数据库(如 DynamoDB)使用替代模型进行数据管理,例如键-值对或文档存储。
Amazon DynamoDB 支持 PartiQL,后者一种与 SQL 兼容的开源查询语言,使您可以轻松、高效地查询数据,无论数据存储在何处或以何种格式存储。使用 PartiQL,您可以轻松处理关系数据库中的结构化数据、采用开放数据格式的半结构化和嵌套数据,甚至可以处理允许不同行中使用不同属性的 NoSQL 或文档数据库中的无模式数据。
Mongo 是目前最流行的面向文档型数据库,
mac 下命令行启动
brew tap mongodb/bew
brew install mongodb-community
brew services start mongodb-community
windows 下在 Mongo/bin 目录下运行
mongo
mongo shell 的命令行
show dbs 列出所有DB
use dbname 切换当前DB
show tables 列出当前DB的所有表/集合
show collections 列出当前DB的所有表/集合
show users 列出当前DB的所有用户
show profile 列出当前DB的所有慢查询
show logs 列出运行日志
执行命令
* db.serverStatus() 查看mongod运行状态信息
* db.stats() 查看db元数据
* db.collection.stats() 查看集合元数据
* db.collection.insert() / update / remove / find 对集合增删改查
* db.collection.createIndex() 创建索引
* db.collection.dropIndex() 删除索引
* db.dropDatabase() 删除DB
* db.printReplicationInfo()
* db.printSlaveReplicationInfo() 查看复制集同步信息
* rs.status() 查看复制集当前状态
* rs.conf() 查看复制集配置
* rs.initiate() 初始化复制集
* rs.reconfig() 重新配置复制集
* rs.add() / rs.remove() 增加/删除复制集节点
* sh.enableSharding() 对DB启用分片
* sh.shardCollection() 对集合进行分片
* sh.status() 查看sharding状态信息
mac 下使用 Robo 3T gui 工具
基本概念
SQL 术语/概念 | MongoDB 术语/概念 | 解析/说明 |
---|---|---|
database |
database |
数据库 |
table |
collection |
数据表/集合 |
row |
document |
数据记录/文档 |
column |
field |
数据记录行/文档 |
index |
index |
索引 |
table |
joins |
表连接,MongoDB 不支持 |
primary key |
primary key |
主键,MongoDB 自动将_id 字段设置为主键 |
查询语句
mongo | sql | 说明 |
---|---|---|
db.users.find() |
select * from users |
从user 表中查询所有数据 |
db.users.find({“username” : “joe”, “age” : 27}) |
select * from users where “username” = “joe” and age = 27 |
查找username = joe 且age = 27 的人 |
db.users.find({}, {“username” : 1, “email” : 1}) |
select username, email from users |
查找username ,email 这2 个子项 |
db.users.find({“age” : {“$gt” : 18}}) |
select * from users where age >18 |
查找age > 18 的会员 |
db.users.find({“age” : {“$gte” : 18}}) |
select * from users where age >=18 |
查找age >= 18 的人 |
db.users.find({“age” : {“$lt” : 18}}) |
select * from users where age <18 |
查找age < 18 的人 |
db.users.find({“age” : {“$lte” : 18}}) |
select * from users where age <=18 |
查找age <= 18 的人 |
db.users.find({“username” : {“$ne” : “joe”}}) |
select * from users where username <> “joe” |
查找 username != joe 的会员 |
db.users.find({“ticket_no” : {“$in” : [725, 542, 390]}}) |
select * from users where ticket_no in (725, 542, 390) |
符合tickt_no 在此范围的结果 |
db.users.find({“ticket_no” : {“$nin” : [725, 542, 390]}}) |
select * from users where ticket_no not in (725, 542, 390) |
符合tickt_no 不在此范围的结果 |
db.users.find({“name” : /joey^/}) |
select * from users where name like “joey%” |
查找前4 个字符为joey 的人 |
删除大量数据,无论是在哪种数据库中,都是一个普遍性的需求。除了正常的业务需求,我们需要通过这种方式来为数据库 “瘦身”。
为什么要 “瘦身” 呢?
表的数据量到达一定量级后,数据量越大,表的查询性能会越差。
毕竟数据量越大,B + 树的层级会越高,需要的 IO 也会越多。
表的数据有冷热之分,将很多无用或很少用到的数据存储在数据库中会消耗数据库的资源。
譬如会占用缓存;会增加备份集的大小,进而影响备份的恢复时间等。
所以,对于那些无用的数据,我们会定期删除。
对于那些很少用到的数据,则会定期归档。归档,一般是将数据写入到归档实例或抽取到大数据组件中。归档完毕后,会将对应的数据从原实例中删除。
一般来说,这种删除操作涉及的数据量都比较大。
对于这类删除操作,很多开发童鞋的实现就是一个简单的 DELETE 操作。看上去,简单明了,干净利落。
但是,这种方式,危害性却极大。
以 MySQL 为例:
会造成大事务
大事务会导致主从延迟,而主从延迟又会影响数据库的高可用切换。
回滚表空间会不断膨胀
在 MySQL 8.0 之前,回滚表空间默认是放到系统表空间中,而系统表空间一旦” 膨胀 “,就不会收缩。
锁定的记录多
相对而言,更容易导致锁等待。
即使是分布式数据库,如 TiDB,如果一次删除了大量数据,这批数据在进行 Compaction 时有可能会触发流控。
所以,对于线上的大规模删除操作,建议分而治之。具体来说,就是批量删除,每次只删除一部分数据,分多次执行。
在 MongoDB 中删除数据,可通过以下三种方式:
db.collection.remove()
删除单个文档或满足条件的所有文档。
db.collection.deleteMany()
删除满足条件的所有文档。
db.collection.bulkWrite()
批量操作接口,可执行批量插入、更新、删除操作。
其中
虽然是批量删除,但在 MySQL 中,如果没控制好节奏,还是很容易导致主从延迟。在 MongoDB 中,其实也有类似的担忧,不过我们可以通过 Write Concern 进行规避。
Write Concern,可理解为写安全策略,简单来说,它定义了一个写操作,需要在几个节点上应用(Apply)完,才会给客户端反馈
https://www.cnblogs.com/ivictor/p/15457454.html
不支持事务
redis 是用 c 语言开发的一个开源的高性能键值对数据库。它通过提供多种键值数据类型来满足不同场景下的存储需求
redis 的应用场景:
缓存(数据查询、短连接、新闻内容、商品内容等等)
分布式集群架构中的 session 分离
聊天室的在线好友列表
任务队列(秒杀、抢购、12306 等等)
应用排行榜
网站访问统计
数据过期处理(精确到毫秒)
使用 linux wget 命令安装
wget http://download.redis.io/releases/redis-3.0.0.tar.gz
将 redis-3.0.0 tar 包拷贝到/usr/local 下
cp redis-3.0.0.tar.gz /usr/local
解压源码
tar -zxvf redis-3.0.0.tar.gz
进入解压后的目录,编译、安装到指定目录
cd /usr/local/redis-3.0.0
make PREFIX=/usr/local/redis install
Redis bin:
redis-benchmark:redis 性能测试工具
redis-check-aof:AOF 文件修复工具
redis-check-rdb:RDB 文件修复工具
redis-cli: redis 命令行客户端
redisconf:redis 配置文件
redis-sentinal:redis 集群管理工具
redis-server:redis
启动服务
redis - server;
redis 有五种数据结构:
string:简单的 kv 缓存。最常规的 set/get 操作,value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。
hash:存放对象。
list:有序列表,可以存储列表型数据
set:无序集合,自动去重
zset:排序的 set,可以自定义排序规则。sorted set 多了一个权重参数 score,集合中的元素能够按 score 进行排列。可以做排行榜应用,取 TOP N 操作。
redis 内部结构
Redis 作为一个内存服务器,它需要处理很多来自外部的网络请求,它使用 I/O 多路复用机制同时监听多个文件描述符的可读和可写状态,一旦收到网络请求就会在内存中快速处理,由于绝大多数的操作都是纯内存的,所以处理的速度会非常地快。
在 Redis 4.0 之后的版本,情况就有了一些变动,新版的 Redis 服务在执行一些命令时就会使用『主处理线程』之外的其他线程,例如 UNLINK
、FLUSHALL ASYNC
、FLUSHDB ASYNC
等非阻塞的删除操作。
为什么大部分命令使用单线程
Redis 从一开始就选择使用单线程模型处理来自客户端的绝大多数网络请求,这种考虑其实是多方面的,作者分析了相关的资料,发现其中最重要的几个原因如下:
Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;而 Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率。
https://draveness.me/whys-the-design-redis-single-thread/
设置过期时间
在设置 key 时为 key 设置一个过期时间,到期后缓存就会失效,
定时删除
redis 默认每秒会进行 10 次过期扫描,但不是扫描所有的 key,而是使用一种简单的贪心政策:
从过期字典里随机选取 20 个 key;
删除这 20 个 key 中已经过期的 key;
如果过期的 key 比率超过 1/4,则重复步骤一。
这样保证了过期扫描不会循环过度,redis 还增加了扫描时间的上限,默认不会超过 25ms
惰性删除
在获取某个 key 时,redis 会检查一下,如果这个 key 设置了过期时间并且已经过期了,就会删除这个 key,不会返回任何数据。
内存淘汰策略
redis 为用户提供了几种内存淘汰策略:
Noeviction:内存不足以容纳新数据时,新写入的数据会报错,del 和 read 请求可以正常执行。这是默认的淘汰策略。
Alleys-lru:当内存不足以容纳新数据时,移除最近最少使用的 key(针对全部的 key,没有设置过期时间的 key 也会被淘汰)
Alleys-random:当内存不足以容纳新数据时,随机移除某个 key(针对全部的 key)
Volatile-lru:当内存不足以容纳新数据时,在设置了过期时间的 key 中,移除最近最少使用的 key
Volatile-random:当内存不足以容纳新数据时,在设置了过期时间的 key 中,随机移除 key
Volatile-ttl:当内存不足以容纳新数据时,在设置了过期时间的 key 中,优先移除有更早过期时间的 key
Redis 有 RDB(快照)和 AOF(日志)两种持久化的机制,RDB 是一次全量备份,AOF 是连续增量备份
RDB 会生成多个数据文件,每个数据文件都代表某个时刻中 redis 的数据。但是使用 RDB 进行持久化是每隔一段时间生成一次数据文件,如果宕机,就会丢失一段时间的数据。
redis 底层使用操作系统的多进程机制 COW 机制来实现 RDB 持久化,redis 在持久化的时候会调用 glibc 的函数 fork 一个子进程,RDB 持久化过程完全交给子进程处理,父进程继续处理客户端的需求。当父进程对某个页面的数据进行修改时,COW 机制会将页面复制一份出来,父页面修改的是复制出来的页面,子进程的页面是没有变化的,还是进程产生的数据,子进程可以安心地遍历数据进行序列化写磁盘,这就是 RDB 持久化被称为快照的原因。
AOF 记录的 redis 服务器的顺序指令序列,只会记录对内存修改的指令记录到日志文件中,在 redis 重启之后就通过 AOF 日志的命令重放,以此来重新构建整个数据集。
AOF 的日志会越来越长,在 redis 重启回复数据时重放整个 AOF 日志也会很耗时,导致长时间 redis 无法对外提供服务,所以 redis 提供了指令对 AOF 日志进行瘦身,进行瘦身时会基于当前内存中的数据,来重写构造一个更小的 AOF 文件,并将旧的膨胀的很大的日志文件删除。
缓存雪崩是指缓存整体崩溃,导致请求全部走到数据库,数据库崩溃同时导致系统崩溃
解决方案:
使用 redis-cluster 实现高可用,防止 redis 崩溃
使用本地缓存 ehcache 和 hystrix 限流、降级等,防止大量请求到后台
使用 redis 持久化,在 redis 崩溃重启后能快速回复数据
缓存穿透:
缓存穿透是指别人发送的恶意请求,每次在缓存和数据库中查询数据都查不到,这样就会导致大量的恶意请求直接将数据库搞崩
解决方案:每次到数据库中查询时就算没查到数据也设置一个空值到缓存中。
缓存击穿:
是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据。
解决方案:
两种方式:
缓存预热
就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis 默认的有 6 中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
两者各有优劣,第一种的缺点是维护大量缓存的 key 是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)
以参考日志级别设置预案:
服务降级的目的,是为了防止 Redis 服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis 出现问题,不去数据库查询,而是直接返回默认值给用户。
1)、存储方式 Memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis 有部份存在硬盘上,redis 可以持久化其数据
2)、数据支持类型 memcached 所有的值均是简单的字符串,redis 作为其替代者,支持更为丰富的数据类型 ,提供 list,set,zset,hash 等数据结构的存储
3)、使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4). value 值大小不同:Redis 最大可以达到 512M;memcache 只有 1mb。
5)redis 的速度比 memcached 快很多
6)Redis 支持数据的备份,即 master-slave 模式的数据备份。
redis 单副本,采用单个 redis 节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。
优点:
缺点:
多副本
redis 多副本,采用主从 (replication) 部署架构,相较于单副本而言最大的特定就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不用物理服务器上,根据公司的基础环境配置,可以实现同时对外提供服务和读写分离策略
优点:
高可靠性:
缺点:
主从复制,读写分离
一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。
Redis 事务功能是通过 MULTI、EXEC、DISCARD 和 WATCH 四个原语实现的
Redis 会将一个事务中的所有命令序列化,然后按顺序执行。
注:redis 的 discard 只是结束本次事务,正确命令造成的影响仍然存在.
1)MULTI 命令用于开启一个事务,它总是返回 OK。MULTI 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。
2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
3)通过调用 DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到 EXEC 命令。
事务包含三个阶段:
Redis 事务包含两种模式 : 事务模式 和 Lua 脚本。
Redis 的事务模式具备如下特点:
但 Lua 脚本更具备实用场景,它是另一种形式的事务,他具备一定的原子性,但脚本报错的情况下,事务并不会回滚。Lua 脚本可以保证隔离性,而且可以完美的支持后面的步骤依赖前面步骤的结果。
Lua 脚本模式的身影几乎无处不在,比如分布式锁、延迟队列、抢红包等场景
使用 Lua 脚本的好处 :
Redis Lua 脚本常用命令:
序号 | 命令及描述 |
---|---|
1 | EVAL script numkeys key [key ...] arg [arg ...] 执行 Lua 脚本。 |
2 | EVALSHA sha1 numkeys key [key ...] arg [arg ...] 执行 Lua 脚本。 |
3 | SCRIPT EXISTS script [script ...] 查看指定的脚本是否已经被保存在缓存当中。 |
4 | SCRIPT FLUSH 从脚本缓存中移除所有脚本。 |
5 | SCRIPT KILL 杀死当前正在运行的 Lua 脚本。 |
6 | SCRIPT LOAD script 将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本。 |
对于 Redis 而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis 的操作之所以是原子性的,是因为 Redis 是单线程的。(Redis 新版本已经引入多线程,这里基于旧版本的 Redis)
Redis 本身提供的所有 API 都是原子操作,Redis 中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 将 get 和 set 改成单命令操作,incr 。使用 Redis 的事务,或者使用 Redis+Lua==的方式实现.
Redis 为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对 Redis 的连接并不存在竞争关系 Redis 中可以使用 SETNX 命令实现分布式锁。
将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作
解锁:使用 del key 命令就能释放锁
解决死锁:
错误:Warning: no config file specified, using the default config. In order to specify a config file
redis-server redis.windows.conf
错误:creating server tcp listening socket 127.0.0.1:6379: bind No error
Redis - cli.exe;
shutdown;
exit;
Redis 3.0 以后,节点之间通过去中心化的方式提供了完整的 sharding、replication (复制机制仍复用原有机制,只是 cluster 具备感知主备的能力)、failover 解决方案,称为 Redis Cluster。即将 proxy/sentinel 的工作融合到了普通的 Redis 节点里。
一个 Redis Cluster 由多个 Redis 节点组构成。不同节点组服务的数据无交集(每一个节点组对应数据 sharding 的一个分片)。节点组内部为主备两类节点,两者数据准实时一致,通过异步化的主备复制机制保证。一个节点组有且仅有一个 master 节点(读写服务),同时有 0 到多个 slave 节点(读服务)。
Redis Cluster 通过分片的方式保存键值对,整个数据库被分为 16384 个槽(slot),数据库中的键都属于这 16384 个槽中的一个,每个节点可以处理 0 个或最多 16384 个槽(每个节点处理的槽都是不相同的)。
https://juejin.cn/post/7032067553291665421
https://www.cnblogs.com/jian0110/p/14002555.html
redis cluster3.0 自带的集群,特点在于他的分布式算法不是一致性 hash,而是 hash 槽的概念,以及自身支持节点设置从节点
Redis 集群有 16384 个哈希槽 , 每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽. 集群的每个节点负责一部分 hash 槽 , 举个例子 , 比如当前集群有 3 个节点 , 那么:
节点 A 包含 0 到 5500 号哈希槽
节点 B 包含 5501 到 11000 号哈希槽.
节点 C 包含 11001 到 16384 号哈希槽.
这种结构很容易添加或者删除节点. 比如如果我想新添加个节点 D, 我需要从节点 A, B, C 中得部分槽到 D 上. 如果我像移除节点 A, 需要将 A 中得槽移到 B 和 C 节点上 , 然后将没有任何槽的 A 节点从集群中移除即可.
由于从一个节点将哈希槽移动到另一个节点并不会停止服务 , 所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型 , 每个节点都会有 N-1 个复制品.
在我们例子中具有 A,B,C 三个节点的集群 , 在没有复制模型的情况下 , 如果节点 B 失败了,那么整个集群就会以为缺少 5501-11000 这个范围的槽而不可用.
然而如果在集群创建的时候(或者过一段时间)我们为每个节点添加一个从节点 A1,B1,C1, 那么整个集群便有三个 master 节点和三个 slave 节点组成,这样在节点 B 失败后,集群便会选举 B1 为新的主节点继续服务,整个集群便不会因为槽找不到而不可用了
不过当 B 和 B1 都失败后,集群是不可用的.
https://blog.51cto.com/server110/1922969
1.twemproxy,大概概念是,它类似于一个代理方式, 使用时在本需要连接 redis 的地方改为连接 twemproxy, 它会以一个代理的身份接收请求并使用一致性 hash 算法,将请求转接到具体 redis,将结果再返回 twemproxy。
缺点:twemproxy 自身单端口实例的压力,使用一致性 hash 后,对 redis 节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。
2.codis,目前用的最多的集群方案,基本和 twemproxy 一致的效果,但它支持在 节点数量改变情况下,旧节点数据可恢复到新 hash 节
https://github.com/RedisInsight/RedisInsight
https://github.com/microsoft/garnet
Garnet 是微软研究院基于 C# .NET8.0 开发的一种新型远程缓存存储系统,它设计目的是实现极速、可扩展和低延迟。Garnet 能够在单节点内进行线程扩展,并支持分片集群执行,具备复制、检查点、故障转移和事务处理功能。它可以在主内存以及分层存储(如 SSD 和 Azure 存储)上运行。Garnet 提供丰富的 API 接口和强大的可扩展性模型。
Garnet 使用 Redis 的 RESP 协议作为其主要通信协议,因此可以使用大多数编程语言中现成的 Redis 客户端,例如 C# 中的 StackExchange.Redis。与其他开源缓存存储相比,Garnet 在性能、延迟、可扩展性和持久性方面都有显著提升。
需要注意的是,Garnet 是微软研究院的一个研究项目,应当作为研究项目来对待。尽管如此,我们是一群对此充满热情的研究人员和开发人员,目前正全职工作于此,以使其尽可能稳定和高效。我们的目标是围绕 Garnet 建立一个活跃的社区。实际上,Garnet 的质量已经足够高,以至于微软的几个一线团队和平台团队已经在内部部署了 Garnet 多个月。