JAVA-and-J2EE

mysql5.5版本数据类型、索引、查询的优化技巧(5.7版本适用)

星期三, 四月 13th, 2022 | Database, JAVA-and-J2EE | 没有评论

mysql5.5版本数据类型、索引、查询的优化技巧(5.7版本适用)

总结一些MySQL的常见使用技巧,以供没有DBA的团队参考。以下内容以MySQL5.5为准,如无特殊说明,存储引擎以InnoDB为准。

最新版8.0已经优化很多了,比如尽量使用子查询等,不断实践才能验证,做好数据压测即可检测.

本文大纲:

MySQL的特点

数据类型优化

索引优化

查询优化

一、MySQL的特点

了解MySQL的特点有助于更好的使用MySQL,MySQL和其它常见数据库最大的不同在于存在存储引擎这个概念,存储引擎负责存储和读取数据。不同的存储引擎具有不同的特点,用户可以根据业务的特点选择适合的存储引擎,甚至是开发一个新的引擎。

MySQL的逻辑架构大致如下:

MySQL默认的存储引擎是InnoDB,该存储引擎的主要特点是:

支持事务处理

支持行级锁

数据存储在表空间中,表空间由一些列数据文件组成

采用MVVC(多版本并发控制)机制实现高并发

表基于主键的聚簇索引建立

支持热备份

其它常见存储引擎特点概述:

MyISAM:老版本MySQL的默认引擎,不支持事务和行级锁,开发者可以手动控制表锁;支持全文索引;崩溃后无法安全恢复;支持压缩表,压缩表数据不可修改,但占用空间较少,可以提高查询性能

Archive:只支持Insert和Select,批量插入很快,通过全表扫描查询数据

SCV:把一个SCV文件当做一个表处理

Memory:数据存储在内存中

还有很多,不再一一列举。

二、数据类型优化

选择数据类型的原则:

选择占用空间小的数据类型

选择简单的类型

避免不必要的可空列

占用空间小的类型更节省硬件资源,如磁盘、内存和CPU。

尽量使用简单的类型,如能用int就不用char,因为后者的排序涉及到字符集的选择,比使用int复杂。

可空列使用更多的存储空间,如果在可空列上创建索引,MySQL需要额外的字节做记录。

创建表时,默认都是可空,容易被开发者忽视,最好是手动改为不可空,如果要存储的数据确实不会有空值的话。

1、整型类型

整型类型包括:

tinyint
 
SMALLINT
 
mediumint
 
INT
 
BIGINT

它们分别使用8、16、24、32和64位存储数字,它们可以表示−2n−1−2n−1到2n−1−12n−1−1范围的数字,前面可以加unsigned修饰,这样可以让正数的可表示范围提高1倍,但是无法表示负数。

另外,为整型指定长度没什么卵用,数据类型定下来,长度也就相应定下来了。

2、小数类型

FLOAT
 
DOUBLE
 
DECIMAL

float和double就是通常意义上的float和double,前者使用32位存储数据,后者使用64位存储数据,和整型一样,为它们指定长度没什么卵用。

decimal类型比较复杂,支持精确计算,占用的空间也大,decimal使用每4个字节表示9个数字,如decimal(18,9)表示数字长度是18,其中小数位9个数字,整数部分9个数字,加上小数点本身,共占用9个字节。

考虑到decimal占用空间较多,以及精度计算很复杂,数据量大的时候可以考虑用bigint代替之,可以在持久化和读取前对真实数据进行一些缩放操作。

3、字符串类型

VARCHAR
 
CHAR
 
varbinary
 
BINARY
 
BLOB
 
text

枚举
varchar类型数据实际占用空间等于字符串的长度加上1个或2个用来记录字符串长度的字节(当row-format没有被设置为fixed时),varchar很节省空间。当表中某列字符串类型的数据长度差别较大时适合使用varchar。

char的实际占用空间是固定的,当表中字符串数据的长度相差无几或很短时适合使用chart类型。

与varchar和char对应的有varbinary和binary,后者存储的是二进制字符串,和前者相比,后者大小写敏感,不用考虑编码方式,执行比较操作时更快。

需要注意的是:虽然varchar(5)和varchar(200)在存储“hello”这个字符串时使用相同的存储空间,但并不意味着将varchar的长度设置太大不会影响性能,实际上,MySQL的某些内部计算,比如创建内存临时表时(某些查询会导致MySQL自动创建临时表),会分配固定大小的空间存放数据。

blob使用二进制字符串保存大文本,text使用字符保存大文本,InnoDB会使用专门的外部存储区来存放此类数据,数据行内仅存放指向他们的指针,此类数据不宜创建索引(要创建也只能正对字符串前缀创建),不过也不会有人这么干。

如果某列字符串大量重复且内容有限,可使用枚举代替,MySQL处理枚举时维护了一个“数字-字符串”表,使用枚举可以减少很多存储空间。

4、时间类型

YEAR
 
DATE
 
TIME
 
datetime
 
TIMESTAMP

datetime存储范围是1001到9999,精确到秒。

timestamp存储1970年1月1日午夜以来的秒数,可以表示到2038年。占用4个字节,是datetime占用空间的一半。timestamp表示的时间和时区有关,另外timestamp列还有个特性,执行insert或update语句时,MySQL会自动更新第一个类型为timestamp的列的数据为当前时间。

很多表中都有设计有一列叫做UpdateTime,这个列使用timestamp倒是挺合适的,会自动更新,前提是系统不会使用到2038年。

5、主键类型的选择

尽可能使用整型,整型占用空间少,还可以设置为自动增长。尤其别使用GUID,MD5等哈希值字符串作为主键,这类字符串随机性很大,由于InnoDB主键默认是聚簇索引列,所以导致数据存储太分散。

另外,InnoDB的二级索引列中默认包含主键列,如果主键太长,也会使得二级索引很占空间。

6、特殊类型的数据

存储IP最好使用32位无符号整型,MySQL提供了函数inet_aton()和inet_ntoa()进行IP地址的数字表示和字符串表示之间的转换。

三、索引优化

InnoDB使用B+树实现索引,举个例子,假设有个People,建表语句如下

CREATE TABLE `people` (
 
  `Id` INT(11) NOT NULL AUTO_INCREMENT,
 
  `Name` VARCHAR(5) NOT NULL,
 
  `Age` tinyint(4) NOT NULL,
 
  `Number` CHAR(5) NOT NULL COMMENT '编号',
 
  PRIMARY KEY (`Id`),
 
  KEY `i_name_age_number` (`Name`,`Age`,`Number`)
 
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8;

插入数据:

它的索引结构大致是这样的:

也就是说,索引列的顺序很重要,如果两行数据的Name列相同,则用Age列比较大小,如果Age列相同,则用Number列比较大小。先用第一列排序,然后是第二列,最后是第三列。

查询的使用应该尽量从左往右匹配,另外,如果左边列范围查找,右边列无法使用索引;还有就是不能隔列查询,否则后面的索引也无法使用到。如以下几个SQL是正面范例:

 
SELECT * FROM people WHERE Name ='Abel' AND Age = 2 AND NUMBER = 12312
 
SELECT * FROM people WHERE Name ='Abel'
 
SELECT * FROM people WHERE Name LIKE 'Abel%'
 
SELECT * FROM people WHERE Name = 'Andy' AND Age BETWEEN 11 AND 20
 
SELECT * FROM people ORDER BY NAME
 
SELECT * FROM people ORDER BY NAME, Age
 
SELECT * FROM people GROUP BY Name

以下几个SQL是反面范例:

 
SELECT * FROM people WHERE Age = 2
 
SELECT * FROM people WHERE NAME LIKE '%B'
 
SELECT * FROM people WHERE age = 2
 
SELECT * FROM people WHERE NAME = 'ABC' AND NUMBER = 3
 
SELECT * FROM people WHERE NAME LIKE 'B%' AND age = 22

1、一个使用Hash值创建索引的技巧

如果表中有一列存储较长字符串,假设名字为URL,在此列上创建的索引比较大,有个办法可以缓解:创建URL字符串的数字哈希值的索引。再新建一个字段,比如叫做URL_CRC,专门放置URL的哈希值,然后给这个字段创建索引,查询时这样写:

 
SELECT * FROM t WHERE URL_CRC = 387695885 AND URL = 'www.baidu.com'

如果数据量比较多,为防止哈希冲突,可自定义哈希函数,或用MD5函数返回值的一部分作为哈希值:

 
SELECT CONV(RIGHT(MD5('www.baidu.com'),16), 16, 10)

2、前缀索引

如果字符串列存储的数据较长,创建的索引也很大,这时可以使用前缀索引,即:只针对字符串前几个字符做索引,这样可以缩短索引的大小,不过,显然,此类索引在执行order by和group by时不起作用。

创建前缀索引时选择前缀长度很重要,在不破坏原来数据分布的情况下尽可能选择较短的前缀。举个例子,如果如果大部分字符串是以”abc”开头,那么如果限定前缀索引长度为4,索引值会包含太多的重复的”abcX”。

3、多列索引

上面提到的“People”上创建的索引即为多列索引,多列索引往往比多个单列索引更好。

对多个索引进行and查询时,应该创建多列索引,而不是多个单列索引。

可以试试这样写的效果:

SELECT * FROM t WHERE f1 = 'v1' AND f2 <> 'v2' UNION ALL SELECT * FROM t WHERE f2 = 'v2' AND f1 <> 'v1'

多列索引的顺序很重要,通常,不考虑排序和分组查询时,应该把选择性(选择性是指某表索引列不同数据的个数/总行数。选择性高意味着重复数据少)大的列放到前面。但也有例外,如果能确认某些查询是频繁执行的,则应该优先照顾这些查询的选择性,比如,如果上面的People表中Name的选择性大于Age,查询语句应该这样写:

SELECT * FROM people WHERE name = 'xxx' AND age = xx

Name列放了索引中的左侧比较合适,但是如果某个SQL执行的评率最高,比如:

SELECT * FROM people WHERE name = 'xxx' AND age = 20

当age=20的记录在数据库中非常少时,反而把age放到索引列的左端效率更高。把age放了索引左端可能对其它age不等于20的查询来说不公平,如果不能确定age=20是最非常频繁的查询条件,还是要综合考虑,把name放了左侧合适。

4、聚簇索引

聚簇索引是一种数据存储结构,InnoDB在主键的索引的叶子节点中直接保存了数据行,而不是像二级索引那样只是保存了索引列的值和所指向行的主键值。由于这个特性,一个表只能有一个聚簇索引。如果一个表没有定义主键也没有定义具有唯一索引的列,那么InnoDB会生成一个隐藏列,并且在此列设为聚簇索引列。

5、覆盖索引
简单地说,某些查询只需要查询索引列,那么就不用再根据索引B树节点记录的主键ID进行二次查询了。

6、重复索引和冗余索引

如果重复在某列创建索引,并不会带来任何好处,只有坏处,应该尽量避免。比如给主键创建唯一索引和普通索引就是多于的,因为InnoDB的主键默认就是聚簇索引了。

冗余索引和重复索引不同,比如某个索引是(A,B),另一个索引是(A),这叫冗余索引,前者可以代替后者,后者不可以代替前者的作用。但是(A,B)和(B)以及(A,B)和(B,A)不算冗余索引,起作用谁也代替不了谁。

如果一个表中已经存在索引(A),现在又想创建索引(A,B),那么只需扩展就的索引就可以,没有必要创建新的索引。需要注意的是如果已经存在索引(A),那么也没有必要在创建索引(A,ID),其中ID指主键,因为索引A默认已经包含了主键了,也算是冗余主键。

但是,有时候,冗余索引也是可取的,假设已经存在索引(A),将其扩展为(A,B)后,因为B列是一个很长的类型,导致用A单独查询时没有以前快了,这时可以考虑新创建索引(A,B)。

7、不使用的索引

不使用的索引徒然增加insert、update和delete的效率,应该及时删除。

8、索引使用总结

索引的三星原则:

索引将查询相关的记录按顺序放在一起则得一星

索引中的数据顺序和查询结果的排序一致则得一星

索引中包含了查询所需要的全部列则得一星

第一个条原则的意思是where条件中查询的顺序和索引是一致的,就是前面说的从左到右使用索引。

索引不是万能的,当数据量巨大时,维护索引本身也是耗费性能的,应该考虑分区分表存储。

四、查询优化
› Continue reading

Tags:

切换阿里云Maven的仓库提升编译速度

星期六, 一月 8th, 2022 | JAVA-and-J2EE, linux | 没有评论

1.更多配置可以可以参考阿里云云效 Maven https://developer.aliyun.com/mvn/guide

2.通常配置如下:修改Maven的settings.xml文件

	<mirrors>  
        <mirror>  
            <id>alimaven</id>  
            <mirrorOf>central</mirrorOf>  
            <name>aliyun maven</name>  
            <url>https://maven.aliyun.com/repository/public/</url>  
        </mirror>  
    </mirrors>

2.或者直接在pom.xml文件中更换

<repositories>
	<repository>
		<id>alimaven</id>
		<name>aliyun maven</name>
		<url>https://maven.aliyun.com/repository/public</url>
	</repository>
</repositories>

Tags:

Mac下遭遇JDK-11.0.1.jdk已经损坏,无法打开修复

星期一, 十二月 13th, 2021 | JAVA-and-J2EE, mac | 没有评论

1.其实不是文件损坏,而是mac的权限管控问题

2.系统偏好设置-安全性和隐私 授予任何程序执行

3.使用对应命令解决

sudo xattr -rc /Library/Java/JavaVirtualMachines/jdk-11.0.1.jdk/
 
./bin/java -version

Tags: ,

centos7安装docker和docker compose快速指引

星期三, 十二月 1st, 2021 | JAVA-and-J2EE, linux | 没有评论

1. 安装之前,先清除之前安装的旧版本docker

sudo yum remove docker docker-client docker-client-latest  docker-common   docker-latest   docker-latest-logrotate   docker-logrotate   docker-selinux   docker-engine-selinux  docker-engine

2. 使用 repository 安装 docker ce

##安装基础依赖包
sudo yum install -y yum-utils device-mapper-persistent-data lvm2  
 
不建议// sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo ## 官方给出的源,国内比较慢,可以用阿里源替代
## 阿里源,国内速度优先
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 
 
## 如果想安装指定版本的docker-ce,可以先用命令查看版本号
sudo yum list docker-ce --showduplicates | sort -r 
 
## 直接运行会默认安装最新版
sudo yum install docker-ce  
## 安装指定版本,例如:yum install docker-ce-20.10.11
sudo yum install docker-ce-<version STRING>  
 
###创建配置信息
sudo mkdir /etc/docker
 
sudo cat > /etc/docker/daemon.json <<eof 
{ 
  "exec-opts": ["native.cgroupdriver=systemd"], 
  "log-driver": "json-file",
  "log-opts": { 
    "max-size": "100m"
   },
  "storage-driver": "overlay2",
  "storage-opts": [ 
    "overlay2.override_kernel_check=true" 
  ] 
} 
EOF

3. 安装 docker-compose不使用pip安装,直接用编译好的

curl -L https://github.com/docker/compose/releases/download/1.29.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

上面安装的是稳定版本1.29.2的 docker-compose,可以到github上找最新版 https://github.com/docker/compose/releases

配置docker-compose命令关联
› Continue reading

Tags: , ,

spring.config.location启动的参数不互补可以使用spring.config.additional-location

星期六, 十一月 27th, 2021 | JAVA-and-J2EE, linux | 没有评论

0.先说下springboot版本为2.5.7,location的会优先使用不再使用打包文件中的配置文件
详情见官方文档:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.external-config.files
解决办法是:

    java -jar -Dspring.config.location=/apps/config/app.properties /app/serverless/app.jar &
  ###替换一下即可
    java -jar -Dspring.config.additional-location=/apps/config/app.properties /app/serverless/app.jar &

1.起因需要给应用加上build time和version对应的版本号

这些参数可以在mvn的时候直接生成出来,但是在配置文件中将无法配置

先把这个做下记录

2.在pom.xml文件的properties中添加如下内容

 
  <properties>
        <maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
        <!--maven.build.timestamp保存了maven编译时间戳-->
        <timestamp>${maven.build.timestamp}</timestamp>
</properties>
 
### 在pom.xml的build中添加如下内容,使properties能取到pom.xml中的数据
<resources>
       <resource>
           <directory>src/main/resources/</directory>
           <filtering>true</filtering>
       </resource>
</resources>

3.在springboot的配置文件中新增

app.name=pomelo
app.build_time=@timestamp@
app.version=@project.version@

4.在spring 应用中使用即可获取打包时间及版本代码如下:
› Continue reading

Tags: ,

ThreadPoolExecutor的详解及自定义阻塞提交的ThreadLocalExcutor实例

星期二, 十一月 2nd, 2021 | JAVA-and-J2EE | 没有评论

ThreadPoolExecutor可以实现线程池的创建。ThreadPoolExecutor相关类图如下:


类图
从类图可以看出,ThreadPoolExecutor最终实现了Executor接口,是线程池创建的真正实现者。

Executor两级调度模型

Executor模型
在HotSpot虚拟机中,Java中的线程将会被一一映射为操作系统的线程。在Java虚拟机层面,用户将多个任务提交给Executor框架,Executor负责分配线程执行它们;在操作系统层面,操作系统再将这些线程分配给处理器执行。

ThreadPoolExecutor的三个角色
任务
ThreadPoolExecutor接受两种类型的任务:Callable和Runnable。

Callable:该类任务有返回结果,可以抛出异常。通过submit方法提交,返回Future对象。通过get获取执行结果。
Runnable:该类任务只执行,无法获取返回结果,在执行过程中无法抛异常。通过execute或submit方法提交。
任务执行器
Executor框架最核心的接口是Executor,它表示任务的执行器。

通过上面类图可以看出,Executor的子接口为ExecutorService。再往底层有两大实现类:ThreadPoolExecutor和ScheduledThreadPoolExecutor(集成自ThreadPoolExecutor)。

执行结果
Future接口表示异步的执行结果,它的实现类为FutureTask。

三个角色之间的处理逻辑图如下:


FutureTask逻辑
线程池处理流程

线程池处理流程
一个线程从被提交(submit)到执行共经历以下流程:

线程池判断核心线程池里是的线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下一个流程;
线程池判断工作队列是否已满。如果工作队列没有满,则将新提交的任务储存在这个工作队列里。如果工作队列满了,则进入下一个流程;
线程池判断其内部线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已满了,则交给饱和策略来处理这个任务。
› Continue reading

Tags:

mac下的VirtualBox安装deepin自定义硬盘大小

星期三, 十月 27th, 2021 | computer, JAVA-and-J2EE, linux | 没有评论

mac下的VirtualBox安装deepin自定义硬盘大小

遇到的一些问题记录下解决过程

0.mac下安装VirtualBox遇到的无法启动问题(6.1.28)

系统偏好设置–》安全性及隐私

通用项目–》运行从以下地址下载的APP 开启任何来源 或者 APPstore和认可的开发者,详情里把 Oracle勾选上去

隐私项目–》辅助功能、完全磁盘访问权限、文件和文件夹 把VirtualBox的完全访问权限加上,其他自选加入不影响

1.下载deepin-desktop-community-20.2.4-amd64.iso文件,初始化硬盘30G大小

遇到全盘安装至少要64G否则无法安装,这里选左侧自定义安装

创建主分区,根目录下,选自动挂载,把30G的硬盘大小拖到最后,貌似至少需要19G。

2.进入不算太漫长的安装等待,一般会在5%的地方等待好久,慢慢等就好了,一般10来分钟即完成安装.

3.安装完成界面很小,不会跟随屏幕自动变大,要安装增强扩展,通过标题栏,一般会报错,找不到对应的iso挂载文件

这里可以看下cd里有没有挂载到VBoxGuestAdditions.iso,没有挂载的话,在cd驱动里选上即可

解决方法:手工挂载执行,找得到文件直接执行即可.

   	sudo su
	cd /media
	mkdir cdrom
	mount /dev/cdrom /media/cdrom
	cd cdrom
	sh VBoxLinuxAdditions.run

› Continue reading

Tags: ,

Presto进程管理实现监控及自动重启

星期四, 十月 21st, 2021 | JAVA-and-J2EE, 大数据 | 没有评论

具体搭建就不说了可以参考官方文档

https://prestodb.io/docs/current/

其他查询接口也列下:

默认UI是:根据更改的端口调整
http://xxx:8080

获取集群状态 接口:
http://xxx/v1/cluster

获取NODE信息:接口:
http://xxx/v1/node

访问/v1/info/state, 直接从worker处获取worker的状态

取各节点的版本详情 获取节点信息的接口:
http://xxx/v1/service

具体监控信息如下:
› Continue reading

Tags:

eclipse2021-06版本使用lombok

星期五, 八月 13th, 2021 | JAVA-and-J2EE | 没有评论

Eclipse IDE for Enterprise Java and Web Developers (includes Incubating components)

Version: 2021-06 (4.20.0)
Build id: 20210612-2011

(c) Copyright Eclipse contributors and others 2000, 2021. All rights reserved. Eclipse and the Eclipse logo are trademarks of the Eclipse Foundation, Inc., https://www.eclipse.org/. The Eclipse logo cannot be altered without Eclipses permission. Eclipse logos are provided for use under the Eclipse logo and trademark guidelines, https://www.eclipse.org/logotm/. Oracle and Java are trademarks or registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.

This product includes software developed by other open source projects including the Apache Software Foundation, https://www.apache.org/.

Lombok v1.18.20 “Envious Ferret” is installed. https://projectlombok.org/

上面是安装完成的版本信息

1.Lombok v1.18.20 下载 当前最新版本 https://projectlombok.org/

2. java -jar lombok.jar

3.安装完毕检查eclipse.ini下配置(需要自行添加–illegal-access=permit)使用jdk16出现的问题

-javaagent:/Applications/Eclipse.app/Contents/Eclipse/lombok.jar
--illegal-access=permit

4.重启eclipse可以正常使用

Tags: ,

Elasticsearch技术使用(ES): 索引别名Aliases问题

星期四, 七月 8th, 2021 | JAVA-and-J2EE, 大数据 | 没有评论

业务问题#

业务需求是不断变化迭代的,也许我们之前写的某个业务逻辑在下个版本就变化了,我们可能需要修改原来的设计,例如数据库可能需要添加一个字段或删减一个字段,而在搜索中也会发生这件事,即使你认为现在的索引设计已经很完美了,在生产环境中,还是有可能需要做一些修改的,需要添加映射字段或者需要修改字段类型等等。

数据库中我们可以直接修改原来的表设计语句,前提是需要做好数据迁移。但是在 Elasticsearch 中就没那么简单了。尽管可以增加新的类型到索引中,或者增加新的字段到类型中,但是不能添加新的分析器或者对现有的字段做改动。如果你那么做的话,结果就是那些已经被索引的数据就不正确,搜索也不能正常工作。针对这个问题必须重新建立索引。

别名定义#

重新建立索引的问题是必须更新应用中的索引名称,索引别名就是用来解决这个问题的!

假设我们有个学生的原始索引 student_index_v1,我们给它起个别名 student_index,程序中也是用别名 student_index 进行搜索,当我们的业务需求发生改变需要修改索引的时候,我们重新创建个索引 student_index_v2,同时将别名 student_index 指向新的索引 student_index_v2,同时将 student_index_v1 的数据迁移到新的 student_index_v2,这样我们就可以做到在零停机下从旧索引切换到新索引。

索引别名就像一个快捷方式或软连接,可以指向一个或多个索引,也可以给任何一个需要索引名的API来使用,而且别名不能与索引同名。

别名带给我们极大的灵活性,允许我们做下面这些:

在运行的集群中可以无缝的从一个索引切换到另一个索引。
给多个索引分组。
给索引的一个子集创建视图。

别名管理#

别名还可以映射到某个索引也可以映射到多个索引。别名还可以与筛选器关联,筛选器将在搜索和路由值时自动应用,别名不能与索引同名。

Elasticsearch 中有两种方式管理别名: _alias 用于单个操作, _aliases 用于执行多个原子级操作。
› Continue reading

Tags: ,

Search

文章分类

Meta