Airthink


  • 首页

  • 分类

  • 归档

  • 标签

  • 关于

  • 搜索

mongo

发表于 2020-05-23 | 分类于 mongo

什么是MongoDB ?

MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统。
在高负载的情况下,添加更多的节点,可以保证服务器性能。
MongoDB 旨在为WEB应用提供可扩展的高性能数据存储解决方案。
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

NoSQL,指的是非关系型的数据库。NoSQL有时也称作Not Only SQL的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称。
NoSQL用于超大规模数据的存储。(例如谷歌或Facebook每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。

主要特点

  • MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。

  • 你可以在MongoDB记录中设置任何属性的索引 (如:FirstName=”Sameer”,Address=”8 Gandhi Road”)来实现更快的排序。

  • 你可以通过本地或者网络创建数据镜像,这使得MongoDB有更强的扩展性。

  • 如果负载的增加(需要更多的存储空间和更强的处理能力) ,它可以分布在计算机网络中的其他节点上这就是所谓的分片。

  • Mongo支持丰富的查询表达式。查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。

  • MongoDb 使用update()命令可以实现替换完成的文档(数据)或者一些指定的数据字段 。

  • Mongodb中的Map/reduce主要是用来对数据进行批量处理和聚合操作。

  • Map和Reduce。Map函数调用emit(key,value)遍历集合中所有的记录,将key与value传给Reduce函数进行处理。

  • Map函数和Reduce函数是使用Javascript编写的,并可以通过db.runCommand或mapreduce命令来执行MapReduce操作。

  • GridFS是MongoDB中的一个内置功能,可以用于存放大量小文件。

  • MongoDB允许在服务端执行脚本,可以用Javascript编写某个函数,直接在服务端执行,也可以把函数的定义存储在服务端,下次直接调用即可。

  • MongoDB支持各种编程语言:RUBY,PYTHON,JAVA,C++,PHP,C#等多种语言。

  • MongoDB安装简单。

    安装

  1. 下载mongodb-linux-x86_64-3.6.3.tar.gz

  2. 解压tar -zxvf mongodb-linux-x86_64-3.0.6.tgz

  3. 在与bin同级的目录下建立data文件夹

  4. mongo.config文件内容

    1
    2
    3
    4
    5
    6
    7
    8
    port=27017
    logpath=/airthink/mongodb-linux-x86_64-3.6.3/mongod.log
    pidfilepath=/airthink/mongodb-linux-x86_64-3.6.3/mongod.pid
    logappend=true
    fork=true
    maxConns=3000
    dbpath=/airthink/mongodb-linux-x86_64-3.6.3/data
    auth=true
  5. 启动./mongodb-linux-x86_64-3.6.3/bin/mongo

    1
    2
    3
    4
    5
    6
    7
    var schema = db.system.version.findOne({_id:"authSchema"});//驱动版本5--SCRAM-SHA-1需要修改成:3--Mongodb-CR
    schema. currentVersion = 3;
    db.system.version.save(schema);

    db.createUser({user:"admin",pwd:"mypass",roles:["readWriteAnyDatabase", "userAdminAnyDatabase", "dbAdminAnyDatabase"]});

    windows下的启动命令到bin目录下 start mongod

    MongoDB用户角色配置

基本知识介绍

MongoDB基本的角色

  1. 数据库用户角色:read、readWrite;
  2. 数据库管理角色:dbAdmin、dbOwner、userAdmin;
  3. 集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager;
  4. 备份恢复角色:backup、restore;
  5. 所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabase
  6. 超级用户角色:root (这里还有几个角色间接或直接提供了系统超级用户的访问(dbOwner 、userAdmin、userAdminAnyDatabase,其中MongoDB默认是没有开启用户认证的,也就是说游客也拥有超级管理员的权限。userAdminAnyDatabase:有分配角色和用户的权限,但没有查写的权限)

连接到MongoDB服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
建库操作开始

./mongodb-linux-x86_64-3.6.3/bin/mongo 进入mongo shell

use admin(使用)

db.createUser({user:"root",pwd:"password",roles:["root"]})//创建root用户

db.createUser({user:"admin",pwd:"admin",roles:[{role:"userAdminAnyDatabase",db:"admin"}]})(创建admin用户)

db.auth('admin', 'admin');(授权)

修改mongod.conf文件

security:
authorization: enabled//启用授权

重启MongoDB服务器

service mongod restart

创建数据库读写权限用户

use admin
db.auth("admin","password");
use ballmatch
db.createUser({
user: "baidu",
pwd: "password",
roles: [{role: "readWrite",db: "baidu"}]
})

Java程序连接MongoDB

1
2
3
4
5
6
7
8
9
10
11
MongoCredential credential = MongoCredential.createCredential("username", "dbName", "password".toCharArray());
ServerAddress serverAddress = new ServerAddress("192.168.10.242", 27017);
MongoClient mongoClient = new MongoClient(serverAddress, Arrays.asList(credential));
DB db = mongoClient.getDB("dbName");
returndb;

//方式二
String sURI = String.format("mongodb://%s:%s@%s:%d/%s", "username", "password", "192.168.10.242", 27017, "dbName");
MongoClientURI uri = new MongoClientURI(sURI);
MongoClient mongoClient = new MongoClient(uri);
DB db = mongoClient.getDB("dbName");

命令介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
修改用户密码
db.updateUser( "admin",{pwd:"password"});

密码认证
db.auth("admin","password");

MongoDB连接信息查询
db.serverStatus().connections;

关闭MongoDB服务
use admin;
db.shutdownServer();

删除用户
删除用户(需要root权限,会将所有数据库中的football用户删除)
db.system.users.remove({user:"baidu"});

删除用户(权限要求没有那么高,只删除本数据中的football用户)
db.dropUser("baidu");

mongoTemplate使用示例

对匹配到的数据进行更新(inc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Query query = new Query();
query.addCriteria(Criteria.where("accountName").is(username));
if (null != password){
query.addCriteria(Criteria.where("accountPwd").is(password));
}
Account account = this.mongoTemplate.findOne(query, Account.class);
if (account != null) {
Update update = new Update();
update.inc("loginTimes", 1);
this.mongoTemplate.updateFirst(query, update, Account.class);//查找并更新
}


mongoTemplate.updateFirst(Query.query(Criteria.where("_id").is(favor.getTypeId())),new Update().inc("favorTimes", 1), News.class);

mongoTemplate.updateFirst(Query.query(Criteria.where("_id").is(favor.getTypeId())),new Update().inc("favorTimes", -1), News.class);

查询一条记录和保存(findOne/save)

1
2
3
4
5
Account account = mongoTemplate.findOne(Query.query(Criteria.where("accountName").is(accountName)),Account.class);

account.setAccountPwd(passwd);

mongoTemplate.save(account);

多条件匹配(并的关系)

1
2
3
4
5
6
7
8
9
Criteria criteria = new Criteria();
criteria.andOperator(Criteria.where("phone").is(phone), Criteria.where("status").is(0));
List<VerifyCode> codes = mongoTemplate.find(Query.query(criteria), VerifyCode.class);

Criteria criteria = new Criteria();
criteria.andOperator(Criteria.where("entityID").is(entityID), Criteria.where("accountType").is(accountType), Criteria.where("weUnionId").ne("").ne(null));
Account Account = mongoTemplate.findOne(Query.query(criteria), Account.class);

Boolean isFavor = mongoTemplate.exists(Query.query(Criteria.where("typeId").is(favor.getTypeId()).and("type").is(favor.getType()).and("userId").is(favor.getUserId())), Favor.class);

多条件匹配(或的关系)

1
2
3
Criteria criteria = new Criteria();
criteria.orOperator(Criteria.where("status").is(1), Criteria.where("status").is(0));
List<VerifyCode> codes = mongoTemplate.find(Query.query(criteria), VerifyCode.class);

根据id查找并删除

1
2
Account account = mongoTemplate.findById(id, Account.class); 
mongoTemplate.remove(account);

计数

1
long count = mongoTemplate.count(Query.query(Criteria.where("accountName").is(accountName)), Account.class);

模糊匹配并排序

1
2
3
Criteria criteria = new Criteria();
criteria.andOperator(Criteria.where("province").regex(province));//正则
mongoTemplate.find(Query.query(criteria).with(new Sort(Direction.ASC, "order")), Bbatch.class);

distinct查询

1
2
3
4
 DBObject dbObject = new BasicDBObject("schoolId", bschool.get_id());
List<String> provinces = mongoTemplate.getCollection(
mongoTemplate.getCollectionName(BschoolScoreline.class)) .distinct("province", dbObject);
bschool.setProvinces(provinces);

查找并所有排序

1
mongoTemplate.find(new Query().with(new Sort(Direction.ASC, "order","createTime")), Bschool.class);

in查询

1
List<MpropValue> mpropValueList = mongoTemplate.find(Query.query(Criteria.where("propId").in(mprop1.get_id())), MpropValue.class);

设置唯一字段

1
2
3
4
5
6
7
8
//used 是1 将其他所有是1的更新为0
if(address.getUsed()==1){
Query q = new Query(Criteria.where("userId").is(user.get_id()));//查询
Update u = new Update().set("used",0);
BulkOperations ops = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, "address");
ops.updateMulti(q,u);//全部更新
ops.execute();
}

插入多条数据

1
2
3
4
5
6
public ReturnStatus upsert(List<BspecialLink> specialLinks) {
ReturnStatus status;
mongoTemplate.insertAll(specialLinks);
status = new ReturnStatus(true);
return status;
}

获取数据库所有集合名称

1
Set<String> sets = mongoTemplage.getDb().getCollectionNames();

查询当天数据

private static Date getStartTime() {
    Calendar todayStart = Calendar.getInstance();
    todayStart.set(Calendar.HOUR_OF_DAY, 0);
    todayStart.set(Calendar.MINUTE, 0);
    todayStart.set(Calendar.SECOND, 0);
    todayStart.set(Calendar.MILLISECOND, 0);
    return todayStart.getTime();
}

private static Date getEndTime() {
    Calendar todayEnd = Calendar.getInstance();
    todayEnd.set(Calendar.HOUR_OF_DAY, 23);
    todayEnd.set(Calendar.MINUTE, 59);
    todayEnd.set(Calendar.SECOND, 59);
    todayEnd.set(Calendar.MILLISECOND, 999);
    return todayEnd.getTime();
}

Criteria criteria = new Criteria();
criteria.andOperator(
        Criteria.where("type").is(type),
        Criteria.where("typeId").is(typeId),
        Criteria.where("userId").is(user.get_id()),
        Criteria.where("createTime").gte(getStartTime())
                .lte(getEndTime()));
return mongoTemplate.exists(Query.query(criteria), Zan.class);

其他

  1. 删除字段

    1
    db.yourcollection.update({},{$unset:{"需要删除的字段":""}},false,true)
  2. 将匹配到的值全部更新

    1
    db.getCollection("bschool").update({"state":0},{$set:{"state":"1"}},{'multi':true})

    db.getCollection(“pdfChapterItem”).update({}, {$rename : {“remark” : “abbr”}}, false, true)//更新所有字段名

  3. 建立索引

    1
    db.bschoolScorelineDetail.ensureIndex({"schoolId":1,"type":1,"batch":1,"province":1},{"name":"schoolScorelineDetail"})

    mongo中建立索引用ensureIndex({“字段1”:1,”字段2”:”-1”},{“name”:”索引名”}) 1和-1代表正序和倒序

  4. mongo数据库备份操作(在mongo的安装目录的bin下执行如下命令)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    不带验证的数据库备份:

    mongodump -d mrmf -o /tools/test mrmf数据库名 /tools/test要备份到的地方

    不带验证的数据库恢复

    mongorestore --db mrmf /tools/test/mrmf mrmf数据库名 /tools/test要恢复的数据

    带验证的数据库备份:

    mongodump -d mrmf -u admin -p "ie8*kskIkd123" --authenticationDatabase=admin -o /tools/test

    带验证的数据库恢复:

    mongorestore --db mrmf -u admin -p "ie8*kskIkd123" --authenticationDatabase=admin /tools/test/red
  5. 开启慢日志

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    在客户端调用db.setProfilingLevel(级别) 命令来实时配置。可以通过db.getProfilingLevel()命令来获取当前的Profile级别。

    > db.setProfilingLevel(2);
    > {"was" : 0 , "ok" : 1}
    > db.getProfilingLevel()
    上面斜体的级别可以取0,1,2 三个值,他们表示的意义如下:
    0 – 不开启
    1 – 记录慢命令 (默认为>100ms)
    2 – 记录所有命令
    Profile 记录在级别1时会记录慢命令,那么这个慢的定义是什么?上面我们说到其默认为100ms,当然有默认就有设置,其设置方法和级别一样有两种,一种是通过添加–slowms启动参数配置。第二种是调用db.setProfilingLevel时加上第二个参数:
    db.setProfilingLevel( level , slowms )
    db.setProfilingLevel( 1 , 10 );
    Mongo Profile 记录是直接存在系统db里的,记录位置 system.profile ,所以,我们只要查询这个Collection的记录就可以获取到我们的 Profile 记录了。

    Profile 信息内容详解:

    ts-该命令在何时执行.

    millis Time-该命令执行耗时,以毫秒记.

    info-本命令的详细信息.

    query-表明这是一个query查询操作.

    ntoreturn-本次查询客户端要求返回的记录数.比如, findOne()命令执行时 ntoreturn 为 1.有limit(n) 条件时ntoreturn为n.

    query-具体的查询条件(如x>3).

    nscanned-本次查询扫描的记录数.

    reslen-返回结果集的大小.

    nreturned-本次查询实际返回的结果集.

    update-表明这是一个update更新操作.

    upsert-表明update的upsert参数为true.此参数的功能是如果update的记录不存在,则用update的条件insert一条记录.

  moved-表明本次update是否移动了硬盘上的数据,如果新记录比原记录短,通常不会移动当前记录,如果新记录比原记录长,那么可能会移动记录到其它位置,这时候会导致相关索引的更新.磁盘操作更多,加上索引更新,会使得这样的操作比较慢.

  insert-这是一个insert插入操作.

  getmore-这是一个getmore 操作,getmore通常发生在结果集比较大的查询时,第一个query返回了部分结果,后续的结果是通过getmore来获取的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#下面是一个超过200ms的查询语句
{
"op" : "query", #操作类型,有insert、query、update、remove、getmore、command
"ns" : "F10data3.f10_2_8_3_jgcc",
"query" : { #具体的查询语句 包括过滤条件,limit行数 排序字段
filter" : {
"jzrq" : {
"$gte" : ISODate("2017-03-31T16:00:00.000+0000"),
"$lte" : ISODate("2017-06-30T15:59:59.000+0000")
},
"jglxfldm" : 10.0
},
"ntoreturn" : 200.0,
"sort" : { #如果有排序 则显示排序的字段 这里是 RsId
"RsId" : 1.0
}
},
"keysExamined" : 0.0, #索引扫描数量 这里是全表扫描,没有用索引 所以是 0
"docsExamined" : 69608.0, #浏览的文档数 这里是全表扫描 所以是整个collection中的全部文档数
"numYield" : 546.0, #该操作为了使其他操作完成而放弃的次数。通常来说,当他们需要访问
还没有完全读入内存中的数据时,操作将放弃。这使得在MongoDB为了
放弃操作进行数据读取的同时,还有数据在内存中的其他操作可以完成。
"locks" : { #锁信息,R:全局读锁;W:全局写锁;r:特定数据库的读锁;w:特定数据库的写锁
"Global" : {
"acquireCount" : {
"r" : NumberLong(1094) #该操作获取一个全局级锁花费的时间。
}
},
"Database" : {
"acquireCount" : {
"r" : NumberLong(547)
}
},
"Collection" : {
"acquireCount" : {
"r" : NumberLong(547)
}
}
},
"nreturned" : 200.0, #返回的文档数量
"responseLength" : 57695.0, #返回字节长度,如果这个数字很大,考虑值返回所需字段
"millis" : 264.0, #消耗的时间(毫秒)
"planSummary" : "COLLSCAN, COLLSCAN", #执行概览 从这里看来 是全表扫描
"execStats" : { #详细的执行计划 这里先略过 后续可以用 explain来具体分析
},
"ts" : ISODate("2017-08-24T02:32:49.768+0000"), #命令执行的时间
"client" : "10.3.131.96", #访问的ip或者主机
"allUsers" : [
],
"user" : ""
}

MongoDB 查询优化

  1.  如果发现 millis 值比较大,那么就需要作优化。
  2.  如果docsExamined数很大,或者接近记录总数(文档数),那么可能没有用到索引查询,而是全表扫描。
  3.  如果keysExamined数为0,也可能是没用索引。
  4.  结合 planSummary 中的显示,上例中是 “COLLSCAN, COLLSCAN” 确认是全表扫描
  5.  如果 keysExamined 值高于 nreturned 的值,说明数据库为了找到目标文档扫描了很多文档。这时可以考虑创建索引来提高效率。
  6.  索引的键值选择可以根据 query 中的输出参考,上例中 filter:包含了 jzrq和jglxfldm 并且按照RsId排序,所以 我们的索引索引可以这么建: db.f10_2_8_3_jgcc.ensureindex({jzrq:1,jglxfldm:1,RsId:1})

聚合操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
示例一
Criteria criteria = new Criteria();
criteria.andOperator(Criteria.where("userId").is(uid), Criteria.where("type").is(2));//查询条件
Aggregation aggregation = Aggregation.newAggregation(Aggregation.match(criteria),Aggregation.group("content").count().as("count"),Aggregation.sort(Sort.Direction.ASC,"count"));
AggregationResults<SearchKeyWords> ar = mongoTemplate.aggregate(aggregation,
Searched.class, SearchKeyWords.class);//Searched为要查询的类,SearchKeyWords为查询结果的vo对象
List<SearchKeyWords> list = ar.getMappedResults();

public class SearchKeyWords {

public String _id;//_id: "中华" 重复的字段
public int count;//count: 57 重复次数
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
示例二
//分组group、排序sort统计每个分类下商品个数
Criteria criteria = new Criteria();
Aggregation aggregation = Aggregation.newAggregation(Aggregation.match(criteria),
Aggregation.group("categoryId").count().as("count"),Aggregation.sort(Sort.Direction.ASC,"count"));

AggregationResults<BasicDBObject> ar = mongoTemplate.aggregate(aggregation,
Goods.class, BasicDBObject.class);
for (BasicDBObject basicDBObject : ar) {
logger.info(JsonUtils.toJson(basicDBObject));
}
//输出结果
2019-05-06 11:13:11 [com.badou.service.basic.goods.GoodsService]-[INFO] {"_id":"6430379335205566832","count":1}//分类id和当前分类下商品个数
2019-05-06 11:13:11 [com.badou.service.basic.goods.GoodsService]-[INFO] {"_id":"8732390723817262538","count":2}
2019-05-06 11:13:11 [com.badou.service.basic.goods.GoodsService]-[INFO] {"_id":"4479460853249390323","count":2}
2019-05-06 11:13:11 [com.badou.service.basic.goods.GoodsService]-[INFO] {"_id":"6571438787886532072","count":16}


示例三
//两个集合关联查询 Aggregation.lookup
//若 A集合关联B集合
//参数: from B集合名
//localField A集合存外键的字段
//foreignField B集合与A集合关联的字段
//as 重新组合成C集合名称
语法
db.collection.aggregate([{
$lookup:
{
from: <collection to join>,
localField: <field from the input documents>,
foreignField: <field from the documents of the "from" collection>,
as: <output array field>
}
}])

Criteria criteria = new Criteria();
criteria.andOperator(Criteria.where("_id").is("6118446118773213048"));
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(criteria),
Aggregation.lookup("goodsCategory", "categoryId", "_id", "goodsCategory"),
Aggregation.unwind("goodsCategory"),
Aggregation.project("title","price").and("goodsCategory.name").as("categoryName").and("goodsCategory.order").as("categoryOrder")
);

AggregationResults<BasicDBObject> ar = mongoTemplate.aggregate(
aggregation, Goods.class, BasicDBObject.class);
for (BasicDBObject basicDBObject : ar) {
logger.info(JsonUtils.toJson(basicDBObject));
}
//输出结果
2019-05-06 21:17:26 [com.badou.service.basic.goods.GoodsService]-[INFO] {"_id":"6118446118773213048","title":"小学生必备国学常识&小学生必读国学经典","price":"10.0","categoryName":"小学生","categoryOrder":"1"}

示例四(未曾验证)
//全文搜索查询
//设置索引:db.logInfo.ensureIndex(msg:"text")
Query query = TextQuery.query(new TextCriteria().matching("coffee").matching("-cake"));
//-cake表示不匹配不包含cake与notMatching类似
//Query query = TextQuery.searching((new TextCriteria().matching("coffee").notMatching("-cake"))
List<LogInfoCol> lfs = mongoTemplate.find(query,LogInfoCol.class);
if(lfs!=null){
for(LogInfoCol logInfoCol:lfs){
logger.info(JSON.toString(logInfoCol));
}
}

示例五(未曾验证)
//得到范围内数据:以Circle构造函数,第一第二个参数为坐标,第三个参数为距离半径查找符合条件的
Circle circle = new Circle(4.2341111,63.00001,0.01);
List<PositionCol> positionCols = mongoTemplate.find(new Query(Criteria.where("location").within(circle)),PositionCol.class);
if(positionCols!=null){
for(PositionCol positionCol:positionCols){
logger.info(JSON.toJSONString(positionCol))
}
}

示例六(未曾验证)
//查找坐标周围10KM内的所有商店
Point location = new Point(116.425253,39.925338);
NearQuery query = NearQuery.near(location).maxDistance(new Distance(10,Metrice.KILOMETERS));
GeoResults<PositionCol> positions = mongoTemplate.geoNear(query,PositionCol.class);
if(positionCols!=null){
for(GeoResults<PositionCol>geoResult :positions){
logger.info(geoResult.getContent().getBuissName()+"-"+geoResult.getDistance().getValue());
}
}
示例七
mongo支持的查询参数实例
fpi.getParams().put("state|integer", "1"); 相当于is 一对一
fpi.getParams().put("ne:state|integer", "1"); 相当于not is 一对一
fpi.getParams().put("all:keywords", keyword); keywords为list 多对一
fpi.getParams().put("in:_id|array", "0,1"); 包含 一对多
fpi.getParams().put("nin:_id|array", "0,1"); 排除id 一对多
fpi.getParams().put("gte:serialNum", start);
fpi.getParams().put("lte:serialNum", end);
//日期查询
Date currentDate = DateUtil.currentDate();
String d = DateUtil.format(currentDate,"yyyy-MM-dd HH:mm:ss");
fpi.getParams().put("gte:endDate|datetime", d); 大于当前日期的翻页查询

聚合查询
1、Criteria idcardCriteria = new Criteria();
idcardCriteria.orOperator(Criteria.where("idcard").regex(studentIdcard.toLowerCase()),
Criteria.where("idcard").regex(studentIdcard.toUpperCase()));
criteria.andOperator(idcardCriteria);

Mongodb: Sort operation used more than the maximum 33554432 bytes of RAM
mongo中所有排序字段加上索引比较好

Criteria criteria = new Criteria();
criteria.andOperator(Criteria.where("shakyId").in(gshakyIds), Criteria.where("userId").in(userIds));//筛选条件(限定数据范围)
Aggregation aggregation = Aggregation.newAggregation(Aggregation.match(criteria),
Aggregation.group("userId", "shakyId").count().as("count"),Aggregation.sort(Sort.Direction.ASC, "count"));//分组 单个字段分组,直接取count;两个字段key 确定唯一一条记录,后续需要手动统计

AggregationResults ar = mongoTemplate.aggregate(aggregation,
Ugclock.class, BasicDBObject.class);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
for (BasicDBObject basicDBObject : ar) {
String id = basicDBObject.getString("shakyId");
int count = 1;
if (shakyCount.containsKey(id)) {
count = shakyCount.get(id);
count++;
shakyCount.put(id, count);
} else {
shakyCount.put(id, count);
}
}

2、以id为group by计算num的总值sum()
Aggregation agg = Aggregation.newAggregation(
Aggregation.group(new String[] {"_id"}).sum("num").as("num")
);
3、以id为group by找出num的最大值max()
Aggregation agg = Aggregation.newAggregation(
Aggregation.group(new String[] {"_id"}).max("num").as("num")
);
Aggregation agg = Aggregation.newAggregation(
Aggregation.group(new String[] {"_id"}).min("num").as("num")
);
4、以id为group by计算num的平均值avg()
Aggregation agg = Aggregation.newAggregation(
Aggregation.group(new String[] {"_id"}).avg("num").as("num")
);

翻页

项目当中模拟插入了120W条数据,在同一个文档当中单纯查询数据的速度还不错,主要是对查询的文档字段添加了索引,但是对查询结果的前台分页确有问题。具体来说是不设置任何查询条件的时候,会查询出来将近120W条满足条件的结果,使用mongodb的limit()和skip() 来取出来 第一页前20条数据,这样在后台的java程序当中只是这20条数据占用内存。
代码具体形式类似于用mongodb客户端执行db.feedbackInfo.find(criteria).skip(0).limit(20) 获得第一页0-20条数据db.feedbackInfo.find(criteria).skip(20).limit(20) 获得第二页20-40条数据……db.feedbackInfo.find(criteria).skip(N).limit(20) 获得第二页N-N+20条数据但问题在于随着不断翻页,skip的值N会越来越大,前台的反应越来越慢。很直接的一个表现就是在前台从第一页直接跳转进入最后一页根本反应不过来。对于这个实际问题,原因就是本书这里所言的skip略过大量结果会带来性能问题,再根源地说是mongodb还不够完善,索引本身还比较简单。具体的这个分页效率的问题,有两种思路:第一,等mongodb升级,优化这个skip的执行效率。第二,不用skip()而实现分页效果。这个思路的基础就是mongodb本身对于where查询和limit()的效率还比较不错,也就是本来分页的那个查询用where和limit速度还可以的前提(一般就是需要建立必要的索引)。假如这个前提不成立,那没法讨论。本书接下来具体讨论了不使用skip对结果分页的实现例子,这个本质是对信息系统增加一个查询中间量——上次查询的业务数值,在逻辑上承担起跟skip相对等价的功能。比如说是第一页查询是按照一个日期date值查询,第一次用db.foo.find().sort({“date”,-1}).limit(20)而点击下一页的时候,事先将上次查询的date的边界值给传递过去,第二页查询的时候就使用新的find条件查询db.foo.find({“date”:{“$gt”:latest.date}}); 而后再对查询结果排序即可这种绕过skip的方式评价:第一,很难比较方便地解决所有的分页问题,简单来说 对于使用正则表达式的查询,根本无法通过记录边界条件来实现。第二,不得不多传递上次查询的那个边界条件,增加了工作量,不够优雅。第三,只能够解决一页一页往下翻页的问题,如果我要从第1页直接跳到100页,就束手无策

翻页的实现一

skip实现跳页(比较简单,数据量大了不行)

 /**
 *db.getCollection('user').find({})是指查询全部。
 *sort()设置排序,本示例是指以_id作为条件,正序排序。若将数字1改为-1,则为倒序。
 *skip()设置跳页,本示例是指跳过前10条,从第11条开始显示。
 *limit()设置每页的显示数量,本例是指每页限制显示10条。
 */
db.getCollection('user').find({}).sort({"ID":1}).skip(10).limit(10)

非skip实现跳页

原理:以自增_id作为主条件,获取前一页的最后一条记录,查询之后的指定条记录

//根据_id,查询前10条
var a = db. getCollection('user').find({}).sort("_id",1).limit(10)
//定义变量last
var last  = null
//循环遍历
while(a.hasNext()){
    last=a.next;//循环到最后,last接收的是最后一条的信息
}
//核心是"_id":{"$gt":last._id},即查询大于最后一条的_id的后10条信息
db.getCollection('slt').find({"_id":{"$gt":last._id}}).sort({_id:1}).limit(10)

翻页的实现二(非skip实现跳页,以自增_id作为主条件)

private List<JSONObject> findOrderList(Map<String,Object> paramOptions,int page,int size,String sidx,String sord) throws Exception {
            //连接数据库
    MongoCollection<Document> mongoCollection = getMdbCollection();
    //进行第一次查询,条件中没有skip
    MongoCursor<Document> iterable = mongoCollection.find().limit(size).sort(new BasicDBObject("_id", 1)).iterator(); 
    //定义orderItems,用以接收查询的信息
    List<JSONObject> orderItems = new ArrayList<JSONObject>();

    //如果只有一页或第一页,则走此条件,否,则走else
        if(page == 1){     
      //遍历        
            while (iterable.hasNext()) {
              Document next = iterable.next();
              JSONObject a =JSONObject.parseObject(next.toJson());
              orderItems.add(a);
            }
     }else{ 
      MongoCursor<Document> iterable2 = mongoCollection.find().limit(size*(page-1)).sort(new BasicDBObject("_id", 1)).iterator(); 
         //定义变量last,用以存储每页的最后一条记录 
     Document last = null; 
     while (iterable2.hasNext()) { 
        last = iterable2.next(); 
     } 
         //定义condition,用以添加【$gt:每页最后一条记录的_id值】,作为查询条件 
      Map<String, Object> condition = new HashMap<>(); 
      if (null != last.get("_id")) { 
        condition.put("$gt",last.get("_id")); 
       } 
      MongoCursor<Document> iterable3 = mongoCollection.find(condition).limit(size).sort(new BasicDBObject("_id", 1)).iterator(); 
      //遍历 
      while (iterable.hasNext()) { 
          Document next = iterable.next(); 
          JSONObject a =JSONObject.parseObject(next.toJson()); 
          orderItems.add(a);
      } 
      return orderItems; 
}
 注:上面是按_id正序排列的,如果想要按照倒序排列,则需要将condition.put("$gt",last.get("_id"))改为condition.put("$lt",last.get("_id")),将sort(new BasicDBObject("_id", 1)改为sort(new BasicDBObject("_id", -1)。

mongo高级

本地搭建副本集

mongo的版本

version 3.6.5

副本集文件夹准备

mongodb3.6 同目录下建立文件夹,存放数据
data/mongo/27017,data/mongo/27018,data/mongo/27019
同目录下建立文件夹存放日志数据
data/mongo/27017log,data/mongo/27018log,data/mongo/27019log

启动

mongod --port 27017 --dbpath /data/mongo/27017 --logpath /data/mongo/27017log/27017.log --replSet rs-local-test --logappend
mongod --port 27018 --dbpath /data/mongo/27018 --logpath /data/mongo/27018log/27018.log --replSet rs-local-test --logappend
mongod --port 27019 --dbpath /data/mongo/27019 --logpath /data/mongo/27019log/27019.log --replSet rs-local-test --logappend

三个服务器启动完毕之后,不要关闭。另开一个cmd窗口,连接到27017端口的服务器(连接其他端口也可以),每次启动,主服务器可能会不一样,如果连接的是主服务器,前缀会变成如下PRIMARY,如果是从服务器,前缀会变成SECONDARY

创建配置文件

创建一个配置文件,在配置文件中列出每一个成员,知道彼此的存在(第二次启动就不需要再配置)

use test
witched to db test
     rs.initiate({
      "_id":"rs-local-test01",
      "members":[
     {"_id":0,"host":"127.0.0.1:27017"},
      {"_id":1,"host":"127.0.0.1:27018"},
      {"_id":2,"host":"127.0.0.1:27019"}
    ]
})

{
"ok" : 1,
"operationTime" : Timestamp(1596076771, 1),
"$clusterTime" : {
    "clusterTime" : Timestamp(1596076771, 1),
        "signature" : {
            "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="
            "keyId" : NumberLong(0)
        }
    }
}

查看状态信息

rs-local-test:OTHER> rs.status()
{
"set" : "rs-local-test",
"date" : ISODate("2020-07-30T02:40:29.243Z"),
"myState" : 1,
"term" : NumberLong(1),
"heartbeatIntervalMillis" : NumberLong(2000),
"optimes" : {
"lastCommittedOpTime" : {
"ts" : Timestamp(1596076823, 1),
"t" : NumberLong(1)
},
"readConcernMajorityOpTime" : {
"ts" : Timestamp(1596076823, 1),
"t" : NumberLong(1)
},
"appliedOpTime" : {
"ts" : Timestamp(1596076823, 1),
"t" : NumberLong(1)
},
"durableOpTime" : {
"ts" : Timestamp(1596076823, 1),
"t" : NumberLong(1)
}
},
"members" : [
{
"_id" : 0,
"name" : "127.0.0.1:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 286,
"optime" : {
"ts" : Timestamp(1596076823, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2020-07-30T02:40:23Z"),
"infoMessage" : "could not find member to sync fr
"electionTime" : Timestamp(1596076782, 1),
"electionDate" : ISODate("2020-07-30T02:39:42Z"),
"configVersion" : 1,
"self" : true
},
{
"_id" : 1,
"name" : "127.0.0.1:27018",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 57,
"optime" : {
"ts" : Timestamp(1596076823, 1),
"t" : NumberLong(1)
},
"optimeDurable" : {
"ts" : Timestamp(1596076823, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2020-07-30T02:40:23Z"),
"optimeDurableDate" : ISODate("2020-07-30T02:40:2
"lastHeartbeat" : ISODate("2020-07-30T02:40:28.47
"lastHeartbeatRecv" : ISODate("2020-07-30T02:40:2
),
"pingMs" : NumberLong(0),
"syncingTo" : "127.0.0.1:27017",
"configVersion" : 1
},
{
"_id" : 2,
"name" : "127.0.0.1:27019",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 57,
"optime" : {
"ts" : Timestamp(1596076823, 1),
"t" : NumberLong(1)
},
"optimeDurable" : {
"ts" : Timestamp(1596076823, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2020-07-30T02:40:23Z"),
"optimeDurableDate" : ISODate("2020-07-30T02:40:2
"lastHeartbeat" : ISODate("2020-07-30T02:40:28.47
"lastHeartbeatRecv" : ISODate("2020-07-30T02:40:2
),
"pingMs" : NumberLong(0),
"syncingTo" : "127.0.0.1:27018",
"configVersion" : 1
}
],
"ok" : 1,
"operationTime" : Timestamp(1596076823, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1596076823, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="
"keyId" : NumberLong(0)
}
}
}

如果是第二次启动,可以直接查看状态信息,不需要在设置配置文件。
查看状态信息,可以看到主服务器和备份服务器:

rs-local-test:SECONDARY>use test
switched to db test
rs-local-test:SECONDARY> db.isMaster()
{
"hosts" : [
"127.0.0.1:27017",
"127.0.0.1:27018",
"127.0.0.1:27019"
],
"setName" : "rs-local-test",
"setVersion" : 1,
"ismaster" : false,
"secondary" : true,
"primary" : "127.0.0.1:27019",
"me" : "127.0.0.1:27017",
"lastWrite" : {
"opTime" : {
"ts" : Timestamp(1596079583, 1),
"t" : NumberLong(5)
},
"lastWriteDate" : ISODate("2020-07-30T03:26:23Z"),
"majorityOpTime" : {
"ts" : Timestamp(1596079583, 1),
"t" : NumberLong(5)
},
"majorityWriteDate" : ISODate("2020-07-30T03:26:23Z")
},
"maxBsonObjectSize" : 16777216,
"maxMessageSizeBytes" : 48000000,
"maxWriteBatchSize" : 100000,
"localTime" : ISODate("2020-07-30T03:26:25.289Z"),
"logicalSessionTimeoutMinutes" : 30,
"minWireVersion" : 0,
"maxWireVersion" : 6,
"readOnly" : false,
"ok" : 1,
"operationTime" : Timestamp(1596079583, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1596079583, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}

读写测试

连接主服务器后,可以写入数据:
重新启动一个cmd,连接一个备份服务器,查看是否数据被复制:
备份节点可能会落后于主节点,可能没有最新写入数据,所以备份节点默认情况下会拒绝读取请求,这是为了保护应用程序,以免意外连接到备份节点,读取到过期数据:

rs-local-test:SECONDARY> db.foo.find()
Error: error: {
        "operationTime" : Timestamp(1596079503, 1),
        "ok" : 0,
        "errmsg" : "not master and slaveOk=false",
        "code" : 13435,
        "codeName" : "NotMasterNoSlaveOk",
        "$clusterTime" : {
                "clusterTime" : Timestamp(1596079503, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

从备份节点读取数据,需要设置标识:
db.getMongo().setSlaveOk();

节点变更

在主节点插入数据后,如果有备份节点服务器没有启动,当在该备份启动后,也可以查询到写入的数据:
不能对备份节点执行写入操作,备份节点只能通过复制功能写入数据,不接受客户端的写入请求:
rs-local-test:SECONDARY>

可以随时添加或删除成员,先按之前的方法创建一个备用服务器,再添加进去:
rs.add(‘127.0.0.1’:4444)
rs是一个全局变量,其中包含与复制相关的辅助函数。
删除成员:
rs.remove(‘127.0.0.1’:4444)

添加或删除完可通过 db.isMaster()查看修改结果
也可rs.config();

rs-local-test:SECONDARY> rs.config()
{
        "_id" : "rs-local-test",
        "version" : 1,
        "protocolVersion" : NumberLong(1),
        "members" : [
                {
                        "_id" : 0,
                        "host" : "127.0.0.1:27017",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                },
                {
                        "_id" : 1,
                        "host" : "127.0.0.1:27018",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                },
                {
                        "_id" : 2,
                        "host" : "127.0.0.1:27019",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                }
        ],
        "settings" : {
                "chainingAllowed" : true,
                "heartbeatIntervalMillis" : 2000,
                "heartbeatTimeoutSecs" : 10,
                "electionTimeoutMillis" : 10000,
                "catchUpTimeoutMillis" : -1,
                "catchUpTakeoverDelayMillis" : 30000,
                "getLastErrorModes" : {

                },
                "getLastErrorDefaults" : {
                        "w" : 1,
                        "wtimeout" : 0
                },
                "replicaSetId" : ObjectId("5f2232e3b3ef9f7db4cb682d")
        }
}

以上参考自 https://www.cnblogs.com/a-horse-mosaic/p/9284297.html

问题汇总

如何开启读写分离

默认情况下驱动是从Replica Set 集群中的 Primary 上进行读写的,应用可以在读多写少的场景下开启读写分离,提高效率。

读取偏好(Read Preference)参数

primary 主节点,默认模式,读操作只在主节点,如果主节点不可用,报错或者抛异常。

primaryPreferred 首选主节点,如果主节点不可用,如故障转移,读操作在从节点。

secondary 从节点,读操作只在从节点,如果从节点不可用,报错或者抛异常。

secondaryPreferred 首选从节点,大多情况下读操作在从节点,特殊情况(如单主节点架构)读操作在主节点。

nearest 最邻近节点,读操作在最邻近的成员,可能是主节点或者从节点

在Mongodb中最多能创建多少集合?

默认情况下,MongoDB 的每个数据库的命名空间保存在一个 16MB 的 .ns 文件中,平均每个命名占用约 628 字节,也即整个数据库的命名空间的上限约为 24000。
每一个集合、索引都将占用一个命名空间。所以,如果每个集合有一个索引(比如默认的 _id 索引),那么最多可以创建 12000 个集合。如果索引数更多,则可创建的集合数就更少了。同时,如果集合数太多,一些操作也会变慢。甚至使得MongoDB集群无法服务的情况发生!

MongoDB有传统数据库的事务和事务回滚么?

没有,请不要把它当成关系型数据库来使用,对于MongoDB集群来说,默认情况下数据也不是强一致性的,而是最终一致性。如果对数据一致性比较敏感建议更改WriteConcern级别,但后果是降低了性能,请酌情考虑。

MongoDB有命名规范么?

  • 不能是空字符串
  • 不能含有.、’’、*、/、\、<、>、:、?、$、\0。建议只使用ASCII码中字母和数字
  • 数据库名区分大小写
  • 数据库名长度最多为64字节
  • 集合名不能包含\0字符,这个字符表示集合名的结束
  • 集合名不能是空字符串””
  • 集合名不能使用系统集合的保留前缀”system.”
  • 集名名中不建议包含字符’$’,虽然很多驱动程序可以支持包含此字符的集合名

MongoDB有系统保留库名么?

  • admin
  • local
  • config

MongoDB有连接池么?

MongoDB驱动中其实已经是一个现成的连接池了,而且线程安全。这个内置的连接池默认初始了100个连接,每一个操作(增删改查等)都会获取一个连接,执行操作后释放连接。
【题外话】请务必记得关闭资源,并且设置合理的池子连接数和超时时间。

内置连接池有多个重要参数,分别是

  • connectionsPerHost:每个主机答应的连接数(每个主机的连接池大小),当连接池被用光时,会被阻塞住,默认值为100
  • threadsAllowedToBlockForConnectionMultiplier:线程队列数,它和上面connectionsPerHost值相乘的结果就是线程队列最大值。如果连接线程排满了队列就会抛出“Out of semaphores to get db”错误,默认值为5,则最多有500个线程可以等待获取连接
  • maxWaitTime: 被阻塞线程从连接池获取连接的最长等待时间(ms)。默认值为120,000
  • connectTimeout:在建立(打开)套接字连接时的超时时间(ms)。默认值为10,000
  • socketTimeout:套接字超时时间(ms)。默认值为0,无限制(infinite)
  • autoConnectRetry:这个控制是否在连接时,会自动重试,2.13驱动已经【废弃】,请使用connectTimeout代替它

连接池的MaximumPoolSize要有个合理值,否则这个值数据量的连接都被占用,后面再有新的连接创建时就要等待了,而不能超出池上限新建连接。除此之外还要设置合理的连接等待,连接超时时间,以防止一个连接占用时间过长,影响其它连接请求。

connectTimeout 和 socketTimeout 的区别

一次完整的请求包括三个阶段:

  • 建立连接
  • 数据传输
  • 断开连接

如果与服务器(这里指数据库)请求建立连接的时间超过ConnectTimeout,就会抛 ConnectionTimeOutException,即服务器连接超时,没有在规定的时间内建立连接。
如果与服务器连接成功,就开始数据传输了。
如果服务器处理数据用时过长,超过了SocketTimeOut,就会抛出SocketTimeOutExceptin,即服务器响应超时,服务器没有在规定的时间内返回给客户端数据。

所以这该死的超时该怎么配?
这里有一份国外写的关于超时的建议:
http://blog.mongolab.com/2013/10/do-you-want-a-timeout/
上文给出的通常情况下:connectTimeout=5000,socketTimeout=0

关于WriteConcern

MongoDB提供了一个配置参数:write concern 来让用户自己衡量性能和写安全。分布式数据库中这样的参数比较常见,记得Cassandra中也有一个类似参数,不过那个好像是要写入几个节点返回成功。其实道理都一样分布式的集群环境考虑到性能因素不能确保每个成员都写入后在返回成功,所以只能交给用户根据实际场景衡量。

  • Unacknowledged
  • 这个级别也属于比较低的级别,以前这个级别是驱动配置的默认级别,不过后来调整成Acknowledged级别。在这个级别下,这个驱动会根据当前系统的网络配置进行网络问题的检测,不等待Mongd的返回。代码测试:本地网络问题是否有异常?本地网络无问题是远程server问题是否异常?
  • Acknowledged
  • 这个级别算是中等级别的配置,这个级别能够拿到mongod的返回信息:dupkey Error,以及一些其他的问题。现在这个级别是驱动的默认级别,估计是10gen公司发现好多人评价Mongodb不靠谱后改的。一般系统这个级别也就够用了。由于默认级别是Acknowledged,内部用getLastError方法检查是否写入成功的时候是也不用设置任何参数,对与Replset来说可以在配置中进行getLastErrorDefaults的配置,如果没有的话默认则是Master收到就ok。
  • Journaled
  • 等到操作记录到Journal Log中才返回操作结果,也就是下一次JournaledLog提交。这种情况可以容忍服务器突然宕机,断电等意外的恢复。出去上边的配置还要在启动mongod的时候加上journaling 参数确保可以使用。commitlog提交间隔时间是可以配置的,单磁盘设备(physical volume, RAID device, or LVM volume)每100ms提交一次,和数据文件刷出相同频率,日志和数据分开磁盘设备的30ms提交一次。在插入数据是如果使用{j:true}则会缩短到已配置的默认设置1/3的时间。
  • Replica Acknowledged
  • 在副本集中如果w设置为2的话则至少已经吸入到一个secondary中,我猜测写入secondary这个级别是Acknowledged级别,majority是多个secondary已经写入。如果手贱设置w参数大于replset中需要复制的secondarys的话,操作就一直等待直到达到已写入数据的服务器数量符合要求,也可以设置timeout值来指明最长等待时间。{ getLastError: 1, w: 2, wtimeout:5000 }

MongoDB的锁机制

MongoDB的锁机制和一般关系数据库如 MySQL(InnoDB), Oracle 有很大的差异,InnoDB 和 Oracle 能提供行级粒度锁,而 MongoDB v2 只能提供库级粒度锁,这意味着当 MongoDB 一个写锁处于占用状态时,其它的读写操作都得干等。

初看起来库级锁在大并发环境下有严重的问题,但是 MongoDB 依然能够保持大并发量和高性能,这是因为 MongoDB 的锁粒度虽然很粗放,但是在锁处理机制和关系数据库锁有很大差异,主要表现在

  • MongoDB 没有完整事务支持,操作原子性只到单个 document 级别,所以通常操作粒度比较小;
  • MongoDB 锁实际占用时间是内存数据计算和变更时间,通常很快;
  • MongoDB 锁有一种临时放弃机制,当出现需要等待慢速 IO 读写数据时,可以先临时放弃,等 IO 完成之后再重新获取锁。

通常不出问题不等于没有问题,如果数据操作不当,依然会导致长时间占用写锁,比如下面提到的前台建索引操作,当出现这种情况的时候,整个数据库就处于完全阻塞状态,无法进行任何读写操作,情况十分严重。

解决问题的方法,尽量避免长时间占用写锁操作,如果有一些集合操作实在难以避免,可以考虑把这个集合放到一个单独的 MongoDB 库里,因为 MongoDB 不同库锁是相互隔离的,分离集合可以避免某一个集合操作引发全局阻塞问题。

建索引导致数据库阻塞

上面提到了 MongoDB 库级锁的问题,建索引就是一个容易引起长时间写锁的问题,MongoDB 在前台建索引时需要占用一个写锁(而且不会临时放弃),如果集合的数据量很大,建索引通常要花比较长时间,特别容易引起问题。

解决的方法很简单,MongoDB 提供了两种建索引的访问,一种是 background 方式,不需要长时间占用写锁,另一种是非 background 方式,需要长时间占用锁。使用 background 方式就可以解决问题。
例如,为超大表 posts 建立索引

//千万不用使用
db.posts.ensureIndex({user_id: 1})
//而应该使用
db.posts.ensureIndex({user_id: 1}, {background: 1})

http://www.111com.net/database/165981.htm

ffmpeg的使用

发表于 2020-04-04 | 分类于 java

视频格式小科普

在开始下面的教程之前有必要先简单科普一下视频格式的知识。

视频格式是一种非常不专业的叫法,事实上,视频有编码格式和容器格式两种。编码格式之于容器格式就像牛奶之于杯子一样。 常见的视频文件有mp4(mpeg4 part 14),mkv,flv等,这些是视频的容器格式/封装格式(Container format)。它们包含视频流和音频流,mkv支持多条音轨和字幕,因此是目前最受欢迎的容器格式。比如在播放mkv视频的时候,可以选择不同语言的音轨/字幕。 至于视频编码格式(Encoding format),则是这些容器所包含的视频流/音频流所采用的压缩方式,比如最常用的h.264/aac,mpeg4/mpeg3等。

安装ffmpeg

在mac os上安装ffmpeg:

brew install ffmpeg –with-fdk-aac –with-libass –with-sdl2
与默认安装相比,这里增加了对fdk-aac音频库和ass字幕库的支持,同时安装了ffplay播放器。

在linux上安装ffmpeg最简单的方式是使用官方提供的静态编译安装包,解压后即可使用。

ffmpeg命令通用格式
先来认识一下ffmpeg命令行工具的格式:

ffmpeg -global_options -input_1_options -i input_1 -input_2_options -i input_2 -output_1_options output_1 …
不管你看到的ffmpeg命令多么复杂,万变不离其宗,都可以按照这个格式拆分成几个单独的部分,各个选项的含义可以查阅ffmpeg的documentation。

需要注意的是: 输入输出 至少有一个输入和输出 可以同时有多个输入和输出 输入、输出不一定是文件,可以是rtmp流的地址、摄像头对应的设备文件等 顺序很重要 所有的输入完了然后是输出 * 某个选项只对它后面的输入或输出起作用

1、查看视频的信息

ffmpeg -hide_banner -i input.mkv 或者 ffprobe -hide_banner input.mkv

通过ffmpeg可以查看一个视频文件的以下基本信息:

视频/音频的编码格式
分辨率(1080p or 1080i, Progressive Interlaced)
时长(duration)/码率(bitrate)/帧率(fps,frames per second)
音轨(Audio)/字幕(Subtitle)

2、转码(transcode)

转码是指对流(视频、音频和字幕)进行解码然后再编码,这个过程非常耗CPU。输出的编码格式通过-codec指定,如果不指定,ffmpeg并不会直接copy,而是采用根据容器格式采用相应的默认编码格式进行重新编码。

为了简单起见,把转换容器格式也归为转码,ffmpeg会通过输出文件的后缀判断输出的容器格式,因此不需要指定(-f)。

视频
将mkv转换成mp4

有时候我们需要将mkv转换成mp4:

很多视频网站不支持上传mkv格式
大部分视频剪辑软件,比如premiere等不支持导入mkv格式的视频
下面的命令将mkv的视频和音频流重新封装成mp4文件,不进行重新编码(无损)

ffmpeg -i input.mkv -codec copy output.mp4
制作mkv

mkv格式能够封装多个音频、字幕轨,是目前最为流行的视频分发格式,通常使用mkvtoolnix工具来编辑制作mkv视频,通过ffmpeg也能完成一些简单的任务。

将mpeg4/mp3编码的视频重新编码为h.264/aac,并和字幕一起封装成mkv文件:

ffmpeg -i input.avi -i input.srt -map 0:0 -map 0:1 -map 1:0 -c:v libx264 -c:a aac -c:s srt output.mkv
这里使用ffmpeg自带的aac音频编码器,而不是fdkaac。

音频
ffmpeg不仅能处理视频,音频文件也不在话下,因此,同样可以用ffmpeg转换音频格式。

将flac无损的音乐转换成高质量(High Quality,即320Kb)的mp3格式:

ffmpeg -i "Michael Jackson - Billie Jean.flac" -ab 320k "Michael Jackson - Billie Jean.mp3"

3、截取(cut)

从视频中截取出精彩片段是我们最常用的功能。

从第2分钟开始,截取30秒。所有流都直接拷贝。

ffmpeg -ss 00:02:00.0 -i input.mkv -t 30 -c copy output.mkv
-ss设置开始时间点,格式是HH:MM:SS.xxx或sec.msec。当作为输入选项时,可以快速seek到指定时间点; -t作为输出选项,设置时长。

截取最大的问题是难以做到精确控制时间点,大部分的视频剪辑软件都存在这样的问题:

视频开头卡顿/不流畅,画面与声音不同步
时间误差甚至能达到秒级,比如截取10秒的片段,可能会得到12秒的输出
原因是ffmpeg会seek到指定时间点前的一个关键帧,如果是copy视频流,seek点和起点之间的额外部分将会被保留,因此就产生了误差。解决办法是将-ss作为输出选项或进行重新编码:

ffmpeg -i input.mkv -ss 00:02:00.0 -t 30 -c copy output.mkv
-ss作为输出选项时,解码到指定的时间点,然后开始输出,相当于要扫描前面的所有帧。但是这种方式能获得更精确的输出。

如果还是出现上述问题,使用下面的终极解决方案(使用两次seek):

ffmpeg -ss 00:01:30.0 -i input.mkv -ss 00:00:30.0 -t 30 output.mkv

4、截图(screenshot)

截图就是将某个视频帧保存为图片,例如:

在指定时间点截图

ffmpeg -ss 00:30:14.435 -i input.mkv -vframes 1 out.png

  • -ss作为输入选项,可以快速定位到指定时间点;如果作为输出选项,需要读取指定时间点前面所有的帧,但可以获得更精确的位置。 * -vframes设置输出的帧数。

结合fps视频滤镜,可以从视频中截取多张图片。例如:

每1分钟截一张图,在截图文件名中添加当前的时间

ffmpeg -i input.mkv -vf fps=1/60 -strftime 1 out_%Y%m%d%H%M%S.jpg

5、压缩(compress)

一张蓝光原盘接近50G,而720p的电影可能只有2G,这是通过压缩实现的。在保证画质的前提下使用更小的文件存储,一直是压缩的目标。然而压缩并非那么简单,比如同样是720p的电影,有的只有2个G,有的却有5个G,然而从视觉上几乎看不出区别来。 这里只介绍压缩的一个简单用途:resize。

将1080p转成720p,宽度自适应

ffmpeg -i input.mkv -c copy -c:v libx264 -vf scale=-2:720 output.mkv
-vf是-filter:v的简写,-filter指定滤镜,:v是流选择器,表示对视频流应用滤镜。scale滤镜后面的参数是w:h,w和h可以包含一些变量,比如原始的宽高分别为iw和ih。当其中一个是负数时,假设为-n,ffmpeg自动使用另一个值按照原始的宽高比(aspect ratio)计算出该值,并且保证计算出来的值能被n整除。

因为scale滤镜是针对未编码的原始数据,所以视频流不能用copy,需要进行重新编码。-c copy -c:v libx264表示视频流使用h.264重新编码,其他流直接copy,顺序不能颠倒。

6、字幕(subtitle)

对于视频发布者,给视频添加字幕很有必要。如果是mkv,可以采用制作mkv的方法将字幕文件封装到mkv文件里。如果是mp4视频,则需要进行烧录(draw),即对视频重新编码的过程中将字幕融入视频流。

ffmpeg -i input.mkv -vf subtitles=input.srt output.mp4
该过程是通过subtitles滤镜完成的,安装ffmpeg的时候需要启用libass库。subtitles的参数是字幕文件,如果是mkv文件则表示使用mkv的默认字幕流。

如果是ass格式的字幕文件,则使用ass滤镜:

ffmpeg -i input.mkv -vf ass=input.ass output.mp4
据我所知,通常ass字幕具有更丰富的样式。

另一种常见的方法是使用-c:s mov_text将字幕流封装进mp4里,它不需要重新编码,但是目前大部分播放器对mp4的软字幕支持不好。

7、提取(extract)

有时候我们需要从电影中提取插曲或从视频中提取背景音乐,通过ffmpeg可以很容易从视频文件中提取音频:

ffmpeg -i cut.mp4 -vn output.mp3
-vn表示不输出视频流。

输出的mp3默认的码率是128kb,可以使用ab控制。结合截取中的-ss和-t还可以只提取一个部分的音频。

从mkv文件中提取字幕也是同样的方法。

8、水印(watermark)

给视频添加水印是通过overlay滤镜实现的,overlay滤镜的作用是将一个视频覆盖另一个视频上面,它有两个输入,前一个是下层(main),后一个是上层(overlaid)。 overlay的参数x:y是上层左上角的坐标,0:0表示位于下层的左上角。它可以包含以下参数:

W(w):下(上)层的宽
H(h):下(上)层的高
将水印/logo添加到视频的右上角,并且和边缘保持5像素的距离:

ffmpeg -i input.mkv -i input.png -filter_complex "overlay=W-w-5:5" -c copy -c:v libx264 output.mkv
input.png是带透明背景的logo,作为上层。

需要注意的是,视频需要重新编码,其他流(音频、字幕)则直接copy,顺序不能颠倒。

9、混流(Muxing)

如果我们拍摄了一段视频,想给它添加背景音乐,最好还是借助视频编辑软件,它能对音乐的开头和结尾分别进行增强和减弱的处理,如果不在意这些细节,完全可以用ffmpeg来代替。

替换音频
ffmpeg -i input.mkv -i input.mp3 -map 0:v -map 1:a -c copy -shortest output.mp4
-map的作用是手动选择输出的流:

-map 0:v – 从输入0(第1个输入,即input.mkv) 选择视频流
-map 1:a – 从输入1(第2个输入,即input.mp3) 选择音频流
第1个map对应输出的第1个流,第2个map对应输出的第2个流,以此类推。

合并音频
另一个常见的形式是将两个音频合并成一个,需要使用amerge滤镜:

ffmpeg -i input.mkv -i output.aac -filter_complex "[0:a][1:a]amerge=inputs=2[a]" -map 0:v -map "[a]" -c:v copy -c:a aac -ac 2 -shortest output.mp4

10、制作gif

结合前面讲到的截取和压缩,将输出文件的后缀改为gif就可以得到动态图

ffmpeg -ss 30 -t 5 -i input.mp4 -r 10 -vf scale=-1:144 -y output.gif
-r指定帧率,原始的帧率是25,降低帧率能减小gif图片的大小。

但是这种方式的效果很差,有一种粗糙的布料的感觉。 原因在于gif限制只能包含256种颜色,而原始视频可能包含数以百万的颜色,ffmpeg默认会使用一种通用的调色板(palettep),因此导致颜色失真。2015年,ffmpeg通过引入palettegen和paletteuse两个滤镜来改进这个问题,它通过扫描整个视频,输出一个最佳的调色板,然后再转换成gif的过程中应用这个调色板从而避免颜色失真。

下面的命令从input.mp4的第30秒开始截取5秒,生成高144像素的gif动态图:

palette="/tmp/palette.png"
filters="fps=10,scale=-1:144:flags=lanczos"

ffmpeg -ss 30 -t 5 -i input.mp4 -vf "$filters,palettegen" -y $palette
ffmpeg -ss 30 -t 5 -i input.mp4 -i $palette -filter_complex "$filters [x]; [x][1:v] paletteuse" -y output.gif

与前一个输出相比,质量提升了很多。

另一个有待研究的难题是如何控制输出gif的大小。在不影响画质的前提下,我们希望体积越小越好。比如网上有的gif图很清晰,却不超过1M。而一段500K的视频文件,使用上面的方法转成gif后输出的gif却有1.6M。

分布式环境下redis的使用优化

发表于 2020-03-10 | 分类于 redis

分布式与集群

单机处理到达瓶颈的时候,你就把单机复制几份,这样就构成了一个“集群”。集群中每台服务器就叫做这个集群的一个“节点”,所有节点构成了一个集群。每个节点都提供相同的服务,那么这样系统的处理能力就相当于提升了好几倍(有几个节点就相当于提升了这么多倍)。但问题是用户的请求究竟由哪个节点来处理呢?最好能够让此时此刻负载较小的节点来处理,这样使得每个节点的压力都比较平均。要实现这个功能,就需要在所有节点之前增加一个“调度者”的角色,用户的所有请求都先交给它,然后它根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。这个“调度者”有个牛逼了名字——负载均衡服务器。集群结构的好处就是系统扩展非常容易。如果随着你们系统业务的发展,当前的系统又支撑不住了,那么给这个集群再增加节点就行了。但是,当你的业务发展到一定程度的时候,你会发现一个问题——无论怎么增加节点,貌似整个集群性能的提升效果并不明显了。这时候,你就需要使用微服务结构了。

从单机结构到集群结构,你的代码基本无需要作任何修改,你要做的仅仅是多部署几台服务器,每台服务器上运行相同的代码就行了。但是,当你要从集群结构演进到微服务结构的时候,之前的那套代码就需要发生较大的改动了。所以对于新系统我们建议,系统设计之初就采用微服务架构,这样后期运维的成本更低。但如果一套老系统需要升级成微服务结构的话,那就得对代码大动干戈了。所以,对于老系统而言,究竟是继续保持集群模式,还是升级成微服务架构,这需要你们的架构师深思熟虑、权衡投入产出比。OK,下面开始介绍所谓的分布式结构。分布式结构就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。

举个例子,假设需要开发一个在线商城。按照微服务的思想,我们需要按照功能模块拆分成多个独立的服务,如:用户服务、产品服务、订单服务、后台管理服务、数据分析服务等等。这一个个服务都是一个个独立的项目,可以独立运行。如果服务之间有依赖关系,那么通过RPC方式调用。这样的好处有很多:系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升。系统之间的耦合度降低,从而系统更易于扩展。我们可以针对性地扩展某些服务。假设这个商城要搞一次大促,下单量可能会大大提升,因此我们可以针对性地提升订单系统、产品系统的节点数量,而对于后台管理系统、数据分析系统而言,节点数量维持原有水平即可。服务的复用性更高。比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。

集群环境下redis锁的进一步优化

死锁的问题

解决:

假如程序被kill了,删除锁同样无法执行得到

解决:给锁增加过期时间

过期时间的确定

假如程序1执行到删除锁的时间大于了过期时间,也就是程序1还没执行完锁就被删掉了,此时另外一个线程也可以拿到锁。当线程1执行完去删除锁的时候删掉的就是线程2的锁了,程序就乱了。

解决:
保证当前线程只删除当前线程加的锁

redisson.org的使用

<dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.6.5</version>
    </dependency>


  @Bean
    @ConditionalOnProperty(name="redisson.address")
    RedissonClient redissonSingle() {
        Config config = new Config();
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(redssionProperties.getAddress())
                .setTimeout(redssionProperties.getTimeout())
                .setConnectionPoolSize(redssionProperties.getConnectionPoolSize())
                .setConnectionMinimumIdleSize(redssionProperties.getConnectionMinimumIdleSize());

        if(StringUtils.isNotBlank(redssionProperties.getPassword())) {
            serverConfig.setPassword(redssionProperties.getPassword());
        }

        return Redisson.create(config);
    }

多核并发缓存架构

发表于 2020-03-04 | 分类于 java

计算机架构

现代计算机模型是基于-冯诺依曼计算机模型,主要包括五大核心部分,1.控制器,2.运算器,3.存储器,4.输入,5.输出。
图如下:

CPU的基本结构及其工作原理

中央处理器(CPU,Central Processing Unit)是一块超大规模的集成电路,是一台计算机的运算核心(Core)和控制核心( Control Unit)。它的功能主要是解释计算机指令以及处理计算机软件中的数据。中央处理器主要包括运算器(算术逻辑运算单元,ALU,ArithmeTIc Logic Unit)和高速缓冲存储器(Cache)及实现它们之间联系的数据(Data)、控制及状态的总线(Bus)。它与内部存储器(Memory)和输入/输出(I/O)设备合称为电子计算机三大核心部件。

CPU的基本结构

从功能上看,一般CPU的内部结构可分为:控制单元、逻辑运算单元、存储单元(包括内部总线和缓冲器)三大部分。其中控制单元完成数据处理整个过程中的调配工作,逻辑单元则完成各个指令以便得到程序最终想要的结果,存储单元就负责存储原始数据以及运算结果。浑然一体的配合使得CPU拥有了强大的功能,可以完成包括浮点、多媒体等指令在内的众多复杂运算,也为数字时代加入了更多的活力。

  逻辑部件

  英文Logic components;运算逻辑部件。可以执行定点或浮点算术运算操作、移位操作以及逻辑操作,也可执行地址运算和转换。

  寄存器

  寄存器部件,包括寄存器、专用寄存器和控制寄存器。 通用寄存器又可分定点数和浮点数两类,它们用来保存指令执行过程中临时存放的寄存器操作数和中间(或最终)的操作结果。 通用寄存器是中央处理器的重要部件之一。

  控制部件

  英文Control unit;控制部件,主要是负责对指令译码,并且发出为完成每条指令所要执行的各个操作的控制信号。

  其结构有两种:一种是以微存储为核心的微程序控制方式;一种是以逻辑硬布线结构为主的控制方式。

  微存储中保持微码,每一个微码对应于一个最基本的微操作,又称微指令;各条指令是由不同序列的微码组成,这种微码序列构成微程序。中央处理器在对指令译码以后,即发出一定时序的控制信号,按给定序列的顺序以微周期为节拍执行由这些微码确定的若干个微操作,即可完成某条指令的执行。

  简单指令是由(3~5)个微操作组成,复杂指令则要由几十个微操作甚至几百个微操作组成。

CPU的逻辑单元

CPU大致可分为如下八个逻辑单元:

指令高速缓存,俗称指令寄存器 : 它是芯片上的指令仓库,有了它CPU就不必停下来查找计算机内存中的指令,从而大幅提高了CPU的运算速度。

译码单元,俗称指令译码器 : 它负责将复杂的机器语言指令解译成运算逻辑单元(ALU)和寄存器能够理解的简单格式,就像一位外交官。

控制单元 : 既然指令可以存入CPU,而且有相应指令来完成运算前的准备工作,背后自然有一个扮演推动作用的角色——它便是负责整个处理过程的操作控制器。根据来自译码单元的指令,它会生成控制信号,告诉运算逻辑单元(ALU)和寄存器如何运算、对什么进行运算以及对结果进行怎样的处理。

寄存器 : 它对于CPU来说非常的重要,除了存放程序的部分指令,它还负责存储指针跳转信息以及循环操作命令,是运算逻辑单元(ALU)为完成控制单元请求的任务所使用的数据的小型存储区域,其数据来源可以是高速缓存、内存、控制单元中的任何一个。

逻辑运算单元(ALU) : 它是CPU芯片的智能部件,能够执行加、减、乘、除等各种命令。此外,它还知道如何读取逻辑命令,如或、与、非。来自控制单元的讯息将告诉运算逻辑单元应该做些什么,然后运算单元会从寄存器中间断或连续提取数据,完成最终的任务。

预取单元 : CPU效能发挥对其依赖非常明显,预取命中率的高低直接关系到CPU核心利用率的高低,进而带来指令执行速度上的不同。根据命令或要执行任务所提出的要求,何时时候,预取单元都有可能从指令高速缓存或计算机内存中获取数据和指令。当指令到达时,预取单元最重要的任务就是确保所有指令均排列正确,然后发送给译码单元。

总线单元 : 它就像一条高速公路,快速完成各个单元间的数据交换,也是数据从内存流进和流出CPU的地方。

数据高速缓存 : 存储来自译码单元专门标记的数据,以备逻辑运算单元使用,同时还准备了分配到计算机不同部分的最终结果。

通过以上介绍可以看出CPU虽小,方寸之地却能容纳大世界,内部更像一个发达的装配工厂,环环相扣,层层相套。正因为有了相互间的协作配合,才使得指令最终得以执行,才构成了图文并茂、影像结合的神奇数字世界。

CPU的工作原理

CPU的根本任务就是执行指令,对计算机来说最终都是一串由“0”和“1”组成的序列。CPU从逻辑上可以划分成3个模块,分别是控制单元、运算单元和存储单元,这三部分由CPU内部总线连接起来。如下所示:

控制单元:控制单元是整个CPU的指挥控制中心,由指令寄存器IR(InstrucTIon Register)、指令译码器ID(InstrucTIon Decoder)和操作控制器OC(OperaTIon Controller)等,对协调整个电脑有序工作极为重要。它根据用户预先编好的程序,依次从存储器中取出各条指令,放在指令寄存器IR中,通过指令译码(分析)确定应该进行什么操作,然后通过操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。操作控制器OC中主要包括节拍脉冲发生器、控制矩阵、时钟脉冲发生器、复位电路和启停电路等控制逻辑。

运算单元:是运算器的核心。可以执行算术运算(包括加减乘数等基本运算及其附加运算)和逻辑运算(包括移位、逻辑测试或两个值比较)。相对控制单元而言,运算器接受控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的,所以它是执行部件。

存储单元:包括CPU片内缓存和寄存器组,是CPU中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU访问寄存器所用的时间要比访问内存的时间短。采用寄存器,可以减少CPU访问内存的次数,从而提高了CPU的工作速度。但因为受到芯片面积和集成度所限,寄存器组的容量不可能很大。寄存器组可分为专用寄存器和通用寄存器。专用寄存器的作用是固定的,分别寄存相应的数据。而通用寄存器用途广泛并可由程序员规定其用途,通用寄存器的数目因微处理器而异。这个是我们以后要介绍这个重点,这里先提一下。

我们将上图细化一下,可以得出CPU的工作原理概括如下:

1、取指令:CPU的控制器从内存读取一条指令并放入指令寄存器。

操作码就是汇编语言里的mov,add,jmp等符号码;操作数地址说明该指令需要的操作数所在的地方,是在内存里还是在CPU的内部寄存器里。

2、指令译码:指令寄存器中的指令经过译码,决定该指令应进行何种操作(就是指令里的操作码)、操作数在哪里(操作数的地址)。

3、 执行指令,分两个阶段“取操作数”和“进行运算”。

4、 修改指令计数器,决定下一条指令的地址。

CPU在运算数据的时候(比如 1+1=2),会首先从CUP寄存器读取数据(速度最快,因为内置在CPU里面),如果没有,就从三级缓存里读取,如果三级缓存也没有,则会经过系统总线及内存总线,从总存储器中读取(此处的总存储器主要是指主内存)。

首先CPU工作的时候,由控制单元充当大脑,负责协调。让运算单元做运算的时候,会首先从最靠近CPU的寄存器(其实是和CPU一体的)上读取数据,在寄存器上有CPU运行的常用指令,如果寄存器上没有想要的数据,则就从三级缓存的L1级缓存中获取,如果L1取到数据了,会加载到寄存器中,再转输给CPU运算单元。如果L1中没有,则从L2级缓存中读取,同理,如果没有,则从L3中取。如果L3中也没有,这个时候,就比较麻烦了。要从主内存中取。而从主内存中取的时候,会经过系统总线及内存总线。这时因受到总线的限制,速度会大大降低。

缓存一致性问题

在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存
(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是
也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一
块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步
回到主内存时以谁的缓存数据为准呢?
为了解决一致性的问题,需要各个处理器访问缓存时都 遵循一些协议,在读写时要根据协议来进行操作。常用的方法是总线加锁或是缓存一致性协议-MESI

这里重点说一个什么是缓存一致性协议mesi:
概念:CPU最小存储单元:缓存行
MESI代表的四种状态:
M:修改
E:独享。互斥
S:共享
I:无效
MESI缓存一致性协议原理:
假如现在有CPU1和CPU2,主内存有变量X= 1 。现在要做 x+1的操作。
如果在变量 x = 1 上加上volatile,则就会触发MESI
当CPU1从主内存中读取到X=1时,CPU1会把此变量标记成独享状态
并监听总线,是否有其它CPU去读取此变量
当CPU2从主内存中读取X=1变量时,CPU1会通过嗅探机制监听到。
此时CPU1的X变量会变成共享状态。继续进行计算,计算完变成X=2。
此时要回写到主内存之前。先锁住缓存行。并标记X变量为修改状态。并向总线发消息。
其它CPU2监听总线时,会监听到,并把X标记成无效状态。
CPU1把变量X=2回写到主内存后,会由修改状态变成独享状态。
此时,如果CPU2如果想修改X变量时,要重启从主内存中读取。然后开始新的轮回。

指令重排序问题

为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执 行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该 结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的 顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不 能靠代码的先后顺序来保证。
与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有 类似的指令重排序(Instruction Reorder)优化

JMM内存模型

java线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

JVM

发表于 2020-02-27 | 分类于 java

Java内存区域

前提

本文讲的基本都是以Sun HotSpot虚拟机为基础的,Oracle收购了Sun后目前得到了两个【Sun的HotSpot和JRockit(以后可能合并这两个),还有一个是IBM的IBMJVM】

Java内存模型

Java程序内存的分配是在JVM虚拟机内存分配机制下完成。

Java内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

简而言之,JMM是jvm的一种规范,定义了JVM的内存模型。她屏蔽了各种硬件和操作系统的访问差异,不是直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致,编译器会对代码指令重排序,处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。

Java数据区域分为为五大数据区域。这些区域各有各的用途,创建及销毁时间。

五大内存区域详解

程序计数器

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

为什么需要程序计数器?

我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需要有一个独立的程序计数器,不同线程之间的程序计数器互不影响,独立存储。

如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native方法,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

Java栈(虚拟机栈)

同计数器一样也为线程私有,生命周期与之相同,就是我们平时说的栈,栈描述的是Java方法执行的内存模型。

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。栈遵循先进后出的原则。

栈帧:是用来存储数据和部分过程结果的数据结构。
栈帧的位置:内存->运行时数据区->某个线程对应的虚拟机栈->here
栈帧大小确定时间:编译期确定,不受运行期数据影响。

平时说的栈一般指局部变量表部分。

局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用reference),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jv’m中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其他基本类型固定占用一个Slot.

reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。

returnAddress:指一条字节码指令的地址。

需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

java虚拟机栈可能出现的两种类型的异常:

1、线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError

2、虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory

本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或c++,我们打开jdk安装目录可以看到也有很多c编写的文件,可能就是native方法所调用的c代码

堆

对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。

注意:它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代,再细致点还有Eden(伊甸园)空间之类的不做深究。

根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

web应用程序new出的对象放在Eden区(8/10),当放满了后会自动开启一个线程执行minor gc

方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

运行时常量池

是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。】

jdk8真正开始废弃永久代,而使用元空间(Metaspace)

GC

GC简介

GC(Garbage Collection):即垃圾回收器,诞生于1960年MIT的Lisp语言,主要是用来回收,释放垃圾占用的空间。

java GC泛指java的垃圾回收机制,该机制是java与C/C++的主要区别之一,我们在日常写java代码的时候,一般都不需要编写内存回收或者垃圾清理的代码,也不需要像C/C++那样做类似delete/free的操作。

为什么需要学习GC

对象的内存分配在java虚拟机的自动内存分配机制下,一般不容易出现内存泄漏问题。但是写代码难免会遇到一些特殊情况,比如OOM神马的。。尽管虚拟机内存的动态分配与内存回收技术很成熟,可万一出现了这样那样的内存溢出问题,那么将难以定位错误的原因所在。

哪些内存要回收

java内存模型中分为五大区域已经有所了解。我们知道程序计数器、虚拟机栈、本地方法栈,由线程而生,随线程而灭,其中栈中的栈帧随着方法的进入顺序的执行的入栈和出栈的操作,一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定下来【忽略JIT编译器做的优化,基本当成编译期间可知】,当方法或线程执行完毕后,内存就随着回收,因此无需关心。

而Java堆、方法区则不一样。方法区存放着类加载信息,但是一个接口中多个实现类需要的内存可能不太一样,一个方法中多个分支需要的内存也可能不一样【只有在运行期间才可知道这个方法创建了哪些对象需要多少内存】,这部分内存的分配和回收都是动态的,gc关注的也正是这部分的内存。

Java堆是GC回收的“重点区域”。堆中基本存放着所有对象实例,gc进行回收前,第一件事就是确认哪些对象存活,哪些死去[即不可能再被引用]

堆的回收区域

为了高效的回收,jvm将堆分为三个区域
1.新生代(Young Generation)NewSize和MaxNewSize分别可以控制年轻代的初始大小和最大的大小
2.老年代(Old Generation)
3.永久代(Permanent Generation)【1.8以后采用元空间,就不在堆中了】

判断对象是否存活算法

1.引用计数算法
早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。
缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。

2.可达性分析算法
目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。
它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。

可作为GC Roots的对象有四种

①虚拟机栈(栈桢中的本地变量表)中的引用的对象,就是平时所指的java对象,存放在堆中

②方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中

③方法区中的常量引用的对象

④本地方法栈中JNI(native方法)引用的对象

要真正宣告对象死亡需经过两个过程。

1.可达性分析后没有发现引用链

2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会使F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。]

垃圾收集算法

1.标记/清除算法【最基础】

2.复制算法

3.标记/整理算法

jvm采用分代收集算法对不同区域采用不同的回收算法。

新生代采用复制算法

新生代中因为对象都是”朝生夕死的”,【深入理解JVM虚拟机上说98%的对象,不知道是不是这么多,总之就是存活率很低】,适用于复制算法【复制算法比较适合用于存活率低的内存区域】。它优化了标记/清除算法的效率和内存碎片问题,且JVM不以5:5分配内存【由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1【原有区域:保留空间】划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年进行分配】。

GC开始时,对象只会存于Eden和From Survivor区域,To Survivor【保留空间】为空。

GC进行时,Eden区所有存活的对象都被复制到To Survivor区,而From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认15是因为对象头中年龄占4bit,新生代每熬过一次垃圾回收,年龄+1),则移到老年代,没有达到则复制到To Survivor。

老年代采用标记/清除算法或标记/整理算法

由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。

枚举根节点算法

GC Roots 被虚拟机用来判断对象是否存活

可达性分析算法需考虑

1.如果方法区几百兆,一个个检查里面的引用,将耗费大量资源。

2.在分析时,需保证这个对象引用关系不再变化,否则结果将不准确。【因此GC进行时需停掉其它所有java执行线程(Sun把这种行为称为‘Stop the World’),即使是号称几乎不会停顿的CMS收集器,枚举根节点时也需停掉线程】

解决办法:实际上当系统停下来后JVM不需要一个个检查引用,而是通过OopMap数据结构【HotSpot的叫法】来标记对象引用。

虚拟机先得知哪些地方存放对象的引用,在类加载完时。HotSpot把对象内什么偏移量什么类型的数据算出来,在jit编译过程中,也会在特定位置记录下栈和寄存器哪些位置是引用,这样GC在扫描时就可以知道这些信息。【目前主流JVM使用准确式GC】

OopMap可以帮助HotSpot快速且准确完成GC Roots枚举以及确定相关信息。但是也存在一个问题,可能导致引用关系变化。

这个时候有个safepoint(安全点)的概念。

HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。 GC时对一个Java线程来说,它要么处在safepoint,要么不在safepoint。

safepoint不能太少,否则GC等待的时间会很久

safepoint不能太多,否则将增加运行GC的负担

安全点主要存放的位置

1:循环的末尾

2:方法临返回前/调用方法的call指令后

3:可能抛异常的位置

Minor GC、Major GC、FULL GC

Minor GC:在年轻代Young space(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC,Minor GC只会清理年轻代.

Major GC:Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。所以有人问的时候需问清楚它指的是full GC还是old GC。

Full GC:full gc是对新生代、老年代、永久代【jdk1.8后没有这个概念了】统一的回收。
jvm调优 目的减少full gc次数,full gc的时候会stw,停掉所有线程,影响用户体验的

Linux查看某个服务JVM的GC和堆内存使用情况

使用 jps 命令查看配置了JVM的服务

Jps(Java Virtual Machine Process Status Tool)是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java JVM进程的一些简单情况。

jps
列出pid和java主类名

jps -l
列出pid和java主类全称
[root@airthink ~]# jps
23393 jar
5401 jenkins.war
29675 Bootstrap
31692 Bootstrap


[root@airthink ~]# jps -l
23393 bubu-0.0.1-SNAPSHOT.jar
5401 /usr/lib/jenkins/jenkins.war
24746 sun.tools.jps.Jps
29675 org.apache.catalina.startup.Bootstrap
31692 org.apache.catalina.startup.Bootstrap

jps -lm
列出皮带、主类全称和应用程序参数
[root@airthink ~]# jps -lm
23393 bubu-0.0.1-SNAPSHOT.jar --server.port=7070
5401 /usr/lib/jenkins/jenkins.war --logfile=/var/log/jenkins/jenkins.log --webroot=/var/cache/jenkins/war --daemon --httpPort=9090 --debug=5 --handlerCountMax=100 --handlerCountMaxIdle=20
29675 org.apache.catalina.startup.Bootstrap start
24908 sun.tools.jps.Jps -lm
31692 org.apache.catalina.startup.Bootstrap start

jps -v
列出pid和JVM参数

[root@airthink ~]# jps -v
23393 jar
24964 Jps -Denv.class.path=.:/usr/local/jdk1.8.0_162/lib:/usr/local/jdk1.8.0_162/jre/lib: -Dapplication.home=/usr/local/jdk1.8.0_162 -Xms8m
5401 jenkins.war -Dcom.sun.akuma.Daemon=daemonized -Djava.awt.headless=true -DJENKINS_HOME=/var/lib/jenkins
29675 Bootstrap -Djava.util.logging.config.file=/airthink/oauth-apache-tomcat-8.5.46/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -Dcatalina.base=/airthink/oauth-apache-tomcat-8.5.46 -Dcatalina.home=/airthink/oauth-apache-tomcat-8.5.46 -Djava.io.tmpdir=/airthink/oauth-apache-tomcat-8.5.46/temp
31692 Bootstrap -Djava.util.logging.config.file=/airthink/apache-tomcat-8.0.50/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dignore.endorsed.dirs= -Dcatalina.base=/airthink/apache-tomcat-8.0.50 -Dcatalina.home=/airthink/apache-tomcat-8.0.50 -Djava.io.tmpdir=/airthink/apache-tomcat-8.0.50/temp

查看某个进程JVM的GC使用情况

jstat -gc 71614 5000

jstat -gc 进程号 刷新时间

S0C:年轻代中第一个survivor(幸存区)的容量 (字节) 
S1C:年轻代中第二个survivor(幸存区)的容量 (字节) 
S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节) 
S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节) 
EC:年轻代中Eden(伊甸园)的容量 (字节) 
EU:年轻代中Eden(伊甸园)目前已使用空间 (字节) 
OC:Old代的容量 (字节) 
OU:Old代目前已使用空间 (字节)
YGC:从应用程序启动到采样时年轻代中gc次数 
YGCT:从应用程序启动到采样时年轻代中gc所用时间(s) 
FGC:从应用程序启动到采样时old代(全gc)gc次数 
FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s) 
GCT:从应用程序启动到采样时gc用的总时间(s) 

查看堆内存使用情况

jmap -heap 进程号

Heap Configuration:   #堆配置情况 

MinHeapFreeRatio         = 40 #堆最小使用比例
MaxHeapFreeRatio         = 70 #堆最大使用比例
MaxHeapSize              = 482344960 (460.0MB)  #堆最大空间
NewSize                  = 10485760 (10.0MB) #新生代初始化大小
MaxNewSize               = 160759808 (153.3125MB) #新生代可使用最大容量大小
OldSize                  = 20971520 (20.0MB) #老生代大小
NewRatio                 = 2 #新生代比例
SurvivorRatio            = 8 #新生代与suvivor的占比
MetaspaceSize            = 21807104 (20.796875MB) #元数据空间初始大小
CompressedClassSpaceSize = 1073741824 (1024.0MB)  #类指针压缩空间大小, 默认为1G
MaxMetaspaceSize         = 17592186044415 MB #元数据空间的最大值, 超过此值就会触发 GC溢出( JVM会动态地改变此值)
G1HeapRegionSize         = 0 (0.0MB) #区块的大小

Heap Usage:
New Generation (Eden + 1 Survivor Space): #新生代大小
   capacity = 14876672 (14.1875MB) #区块最大可使用大小
   used     = 2722520 (2.5963973999023438MB) #区块已使用内存
   free     = 12154152 (11.591102600097656MB) #区块空闲内存
   18.30059841340859% used #区块使用比例
Eden Space: # Eden区空间
   capacity = 13238272 (12.625MB)
   used     = 2630736 (2.5088653564453125MB)
   free     = 10607536 (10.116134643554688MB)
   19.87220084313119% used
From Space:  #Survivor0区
   capacity = 1638400 (1.5625MB)
   used     = 91784 (0.08753204345703125MB)
   free     = 1546616 (1.4749679565429688MB)
   5.60205078125% used
To Space: #Survivor1区
   capacity = 1638400 (1.5625MB)
   used     = 0 (0.0MB)
   free     = 1638400 (1.5625MB)
   0.0% used
tenured generation: #老年代
   capacity = 33013760 (31.484375MB)
   used     = 26392512 (25.16986083984375MB)
   free     = 6621248 (6.31451416015625MB)
   79.94397487593052% used

linux服务器或tomcat项目启动,进行jvm参数调优设置

首先执行命令:free -h,查询当前的内存占用情况

[root@airthink ~]# free -h
              total        used        free      shared  buff/cache   available
Mem:           1.8G        1.1G        111M        680K        553M        498M
Swap:            0B          0B          0B

开始进行优化,执行命令:top,查看各个应用的内存占用情况,选取内存占用过高的pid进程;

然后获取pid号31692,根据pid查询对应的进程以及项目路径,执行命令:
ps -aux |grep -v grep|grep 31692

[root@airthink ~]# ps -aux |grep -v grep|grep 31692
root     31692  0.1 17.2 2575436 324396 ?      Sl   Feb24  16:53 /usr/local/jdk1.8.0_162/jre/bin/java -Djava.util.logging.config.file=/airthink/apache-tomcat-8.0.50/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dignore.endorsed.dirs= -classpath /airthink/apache-tomcat-8.0.50/bin/bootstrap.jar:/airthink/apache-tomcat-8.0.50/bin/tomcat-juli.jar -Dcatalina.base=/airthink/apache-tomcat-8.0.50 -Dcatalina.home=/airthink/apache-tomcat-8.0.50 -Djava.io.tmpdir=/airthink/apache-tomcat-8.0.50/temp org.apache.catalina.startup.Bootstrap start

定位到项目跟路径之后,开始设置项目启动jvm内存占用,不同项目可分配不同的内存

如果是springboot项目jar启动,则在启动的时候指定jvm的内存分配:
java -Xms128m -Xmx256m -jar xxx.jar --server.port =8080

如果是tomcat项目启动,则在bin目录下,执行命令:vim catalina.sh,然后在顶部加上:
JAVA_OPTS="-Xms128m -Xmx256m"

uni-app

发表于 2020-02-18 | 分类于 uni-app

uni-app的基本使用

课程介绍:

基础部分:

  • 环境搭建
  • 页面外观配置
  • 数据绑定
  • uni-app的生命周期
  • 组件的使用
  • uni-app中样式学习
  • 在uni-app中使用字体图标和开启scss
  • 条件注释跨端兼容
  • uni中的事件
  • 导航跳转
  • 组件创建和通讯,及组件的生命周期
  • uni-app中使用uni-ui库

项目:黑马商城项目

uni-app介绍 官方网页

uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉)等多个平台。

即使不跨端,uni-app同时也是更好的小程序开发框架。

具有vue和微信小程序的开发经验,可快速上手uni-app

为什么要去学习uni-app?

相对开发者来说,减少了学习成本,因为只学会uni-app之后,即可开发出iOS、Android、H5、以及各种小程序的应用,不需要再去学习开发其他应用的框架,相对公司而言,也大大减少了开发成本。

环境搭建

安装编辑器HbuilderX 下载地址

HBuilderX是通用的前端开发工具,但为uni-app做了特别强化。

下载App开发版,可开箱即用

安装微信开发者工具 下载地址

利用HbuilderX初始化项目
  • 点击HbuilderX菜单栏文件>项目>新建

  • 选择uni-app,填写项目名称,项目创建的目录

运行项目

在菜单栏中点击运行,运行到浏览器,选择浏览器即可运行

在微信开发者工具里运行:进入hello-uniapp项目,点击工具栏的运行 -> 运行到小程序模拟器 -> 微信开发者工具,即可在微信开发者工具里面体验uni-app

在微信开发者工具里运行:进入hello-uniapp项目,点击工具栏的运行 -> 运行到手机或模拟器 -> 选择调式的手机

注意:

  • 如果是第一次使用,需要先配置小程序ide的相关路径,才能运行成功
  • 微信开发者工具在设置中安全设置,服务端口开启
介绍项目目录和文件作用

pages.json 文件用来对 uni-app 进行全局配置,决定页面文件的路径、窗口样式、原生的导航栏、底部的原生tabbar 等

manifest.json 文件是应用的配置文件,用于指定应用的名称、图标、权限等。

App.vue是我们的跟组件,所有页面都是在App.vue下进行切换的,是页面入口文件,可以调用应用的生命周期函数。

main.js是我们的项目入口文件,主要作用是初始化vue实例并使用需要的插件。

uni.scss文件的用途是为了方便整体控制应用的风格。比如按钮颜色、边框风格,uni.scss文件里预置了一批scss变量预置。

就是打包目录,在这里有各个平台的打包文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

```pages``` 所有的页面存放目录

```static``` 静态资源目录,例如图片等

```components``` 组件存放目录

为了实现多端兼容,综合考虑编译速度、运行性能等因素,`uni-app` 约定了如下开发规范:

- 页面文件遵循 [Vue 单文件组件 (SFC) 规范](https://vue-loader.vuejs.org/zh/spec.html)
- 组件标签靠近小程序规范,详见[uni-app 组件规范](https://uniapp.dcloud.io/component/README)
- 接口能力(JS API)靠近微信小程序规范,但需将前缀 `wx` 替换为 `uni`,详见[uni-app接口规范](https://uniapp.dcloud.io/api/README)
- 数据绑定及事件处理同 `Vue.js` 规范,同时补充了App及页面的生命周期
- 为兼容多端运行,建议使用flex布局进行开发

#### 全局配置和页面配置

##### 通过globalStyle进行全局配置

用于设置应用的状态栏、导航条、标题、窗口背景色等。[详细文档](https://uniapp.dcloud.io/collocation/pages?id=globalstyle)

| 属性 | 类型 | 默认值 | 描述 |
| ---------------------------- | -------- | ------- | ---------------------------------------- |
| navigationBarBackgroundColor | HexColor | #F7F7F7 | 导航栏背景颜色(同状态栏背景色) |
| navigationBarTextStyle | String | white | 导航栏标题颜色及状态栏前景颜色,仅支持 black/white |
| navigationBarTitleText | String | | 导航栏标题文字内容 |
| backgroundColor | HexColor | #ffffff | 窗口的背景色 |
| backgroundTextStyle | String | dark | 下拉 loading 的样式,仅支持 dark / light |
| enablePullDownRefresh | Boolean | false | 是否开启下拉刷新,详见[页面生命周期](https://uniapp.dcloud.io/use?id=%e9%a1%b5%e9%9d%a2%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f)。 |
| onReachBottomDistance | Number | 50 | 页面上拉触底事件触发时距页面底部距离,单位只支持px,详见[页面生命周期](https://uniapp.dcloud.io/use?id=%e9%a1%b5%e9%9d%a2%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f) |

##### 创建新的message页面

右键pages新建message目录,在message目录下右键新建.vue文件,并选择基本模板

```html
<template>
<view>
这是信息页面
</view>
</template>

<script>
</script>

<style>
</style>
通过pages来配置页面
属性 类型 默认值 描述
path String 配置页面路径
style Object 配置页面窗口表现,配置项参考 pageStyle

pages数组数组中第一项表示应用启动页

1
2
3
4
5
6
7
8
9
10
11
"pages": [ 、
{
"path":"pages/message/message"
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
]

通过style修改页面的标题和导航栏背景色,并且设置h5下拉刷新的特有样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path":"pages/message/message",
"style": {
"navigationBarBackgroundColor": "#007AFF",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true,
"disableScroll": true,
"h5": {
"pullToRefresh": {
"color": "#007AFF"
}
}
}
}
]
配置tabbar

如果应用是一个多 tab 应用,可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页。

Tips

  • 当设置 position 为 top 时,将不会显示 icon
  • tabBar 中的 list 是一个数组,只能配置最少2个、最多5个 tab,tab 按数组的顺序排序。

属性说明:

属性 类型 必填 默认值 描述 平台差异说明
color HexColor 是 tab 上的文字默认颜色
selectedColor HexColor 是 tab 上的文字选中时的颜色
backgroundColor HexColor 是 tab 的背景色
borderStyle String 否 black tabbar 上边框的颜色,仅支持 black/white App 2.3.4+ 支持其他颜色值
list Array 是 tab 的列表,详见 list 属性说明,最少2个、最多5个 tab
position String 否 bottom 可选值 bottom、top top 值仅微信小程序支持

其中 list 接收一个数组,数组中的每个项都是一个对象,其属性值如下:

属性 类型 必填 说明
pagePath String 是 页面路径,必须在 pages 中先定义
text String 是 tab 上按钮文字,在 5+APP 和 H5 平台为非必填。例如中间可放一个没有文字的+号图标
iconPath String 否 图片路径,icon 大小限制为40kb,建议尺寸为 81px * 81px,当 postion 为 top 时,此参数无效,不支持网络图片,不支持字体图标
selectedIconPath String 否 选中时的图片路径,icon 大小限制为40kb,建议尺寸为 81px * 81px ,当 postion 为 top 时,此参数无效

案例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"tabBar": {
"list": [
{
"text": "首页",
"pagePath":"pages/index/index",
"iconPath":"static/tabs/home.png",
"selectedIconPath":"static/tabs/home-active.png"
},
{
"text": "信息",
"pagePath":"pages/message/message",
"iconPath":"static/tabs/message.png",
"selectedIconPath":"static/tabs/message-active.png"
},
{
"text": "我们",
"pagePath":"pages/contact/contact",
"iconPath":"static/tabs/contact.png",
"selectedIconPath":"static/tabs/contact-active.png"
}
]
}
condition启动模式配置

启动模式配置,仅开发期间生效,用于模拟直达页面的场景,如:小程序转发后,用户点击所打开的页面。

属性说明:

属性 类型 是否必填 描述
current Number 是 当前激活的模式,list节点的索引值
list Array 是 启动模式列表

list说明:

属性 类型 是否必填 描述
name String 是 启动模式名称
path String 是 启动页面路径
query String 否 启动参数,可在页面的 onLoad 函数里获得

组件的基本使用

uni-app提供了丰富的基础组件给开发者,开发者可以像搭积木一样,组合各种组件拼接称自己的应用

uni-app中的组件,就像 HTML 中的 div 、p、span 等标签的作用一样,用于搭建页面的基础结构

text文本组件的用法
001 - text 组件的属性
属性 类型 默认值 必填 说明
selectable boolean false 否 文本是否可选
space string . 否 显示连续空格,可选参数:ensp、emsp、nbsp
decode boolean false 否 是否解码
  • text 组件相当于行内标签、在同一行显示
  • 除了文本节点以外的其他节点都无法长按选中
002 - 代码案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<view>
<!-- 长按文本是否可选 -->
<text selectable='true'>来了老弟</text>
</view>

<view>
<!-- 显示连续空格的方式 -->
<view>
<text space='ensp'>来了 老弟</text>
</view>
<view>
<text space='emsp'>来了 老弟</text>
</view>
<view>
<text space='nbsp'>来了 老弟</text>
</view>
</view>

<view>
<text>skyblue</text>
</view>

<view>
<!-- 是否解码 -->
<text decode='true'>&nbsp; &lt; &gt; &amp; &apos; &ensp; &emsp;</text>
</view>
view视图容器组件的用法

View 视图容器, 类似于 HTML 中的 div

001 - 组件的属性

002 - 代码案例
1
2
3
4
5
<view class="box2" hover-class="box2_active">
<view class='box1' hover-class='active' hover-stop-propagation :hover-start-time="2000" :hover-stay-time='2000'>

</view>
</view>
button按钮组件的用法
001 - 组件的属性
属性名 类型 默认值 说明
size String default 按钮的大小
type String default 按钮的样式类型
plain Boolean false 按钮是否镂空,背景色透明
disabled Boolean false 是否按钮
loading Boolean false 名称是否带 loading t图标
  • button 组件默认独占一行,设置 size 为 mini 时可以在一行显示多个
002 - 案例代码
1
2
3
4
5
<button size='mini' type='primary'>前端</button>

<button size='mini' type='default' disabled='true'>前端</button>

<button size='mini' type='warn' loading='true'>前端</button>
image组件的使用
image

图片。

属性名 类型 默认值 说明 平台差异说明
src String 图片资源地址
mode String ‘scaleToFill’ 图片裁剪、缩放的模式

Tips

  • <image> 组件默认宽度 300px、高度 225px;
  • src 仅支持相对路径、绝对路径,支持 base64 码;
  • 页面结构复杂,css样式太多的情况,使用 image 可能导致样式生效较慢,出现 “闪一下” 的情况,此时设置 image{will-change: transform} ,可优化此问题。

uni-app中的样式

  • rpx 即响应式px,一种根据屏幕宽度自适应的动态单位。以750宽的屏幕为基准,750rpx恰好为屏幕宽度。屏幕变宽,rpx 实际显示效果会等比放大。

  • 使用@import语句可以导入外联样式表,@import后跟需要导入的外联样式表的相对路径,用;表示语句结束

  • 支持基本常用的选择器class、id、element等

  • 在 uni-app 中不能使用 * 选择器。

  • page 相当于 body 节点

  • 定义在 App.vue 中的样式为全局样式,作用于每一个页面。在 pages 目录下 的 vue 文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖 App.vue 中相同的选择器。

  • uni-app 支持使用字体图标,使用方式与普通 web 项目相同,需要注意以下几点:

    • 字体文件小于 40kb,uni-app 会自动将其转化为 base64 格式;

    • 字体文件大于等于 40kb, 需开发者自己转换,否则使用将不生效;

    • 字体文件的引用路径推荐使用以 ~@ 开头的绝对路径。

      1
      2
      3
      4
      @font-face {
      font-family: test1-icon;
      src: url('~@/static/iconfont.ttf');
      }
  • 如何使用scss或者less

uni-app中的数据绑定

在页面中需要定义数据,和我们之前的vue一摸一样,直接在data中定义数据即可

1
2
3
4
5
6
7
export default {
data () {
return {
msg: 'hello-uni'
}
}
}
插值表达式的使用
  • 利用插值表达式渲染基本数据

    1
    <view>{{msg}}</view>
  • 在插值表达式中使用三元运算

    1
    <view>{{ flag ? '我是真的':'我是假的' }}</view>
  • 基本运算

    1
    <view>{{1+1}}</view>
v-bind动态绑定属性

在data中定义了一张图片,我们希望把这张图片渲染到页面上

1
2
3
4
5
6
7
export default {
data () {
return {
img: 'http://destiny001.gitee.io/image/monkey_02.jpg'
}
}
}

利用v-bind进行渲染

1
<image v-bind:src="img"></image>

还可以缩写成:

1
<image :src="img"></image>
v-for的使用

data中定以一个数组,最终将数组渲染到页面上

1
2
3
4
5
6
7
8
9
10
data () {
return {
arr: [
{ name: '刘能', age: 29 },
{ name: '赵四', age: 39 },
{ name: '宋小宝', age: 49 },
{ name: '小沈阳', age: 59 }
]
}
}

利用v-for进行循环

1
<view v-for="(item,i) in arr" :key="i">名字:{{item.name}}---年龄:{{item.age}}</view>

uni中的事件

事件绑定

在uni中事件绑定和vue中是一样的,通过v-on进行事件的绑定,也可以简写为@

1
<button @click="tapHandle">点我啊</button>

事件函数定义在methods中

1
2
3
4
5
methods: {
tapHandle () {
console.log('真的点我了')
}
}
事件传参
  • 默认如果没有传递参数,事件函数第一个形参为事件对象

    1
    2
    3
    4
    5
    6
    7
    8
    // template
    <button @click="tapHandle">点我啊</button>
    // script
    methods: {
    tapHandle (e) {
    console.log(e)
    }
    }
  • 如果给事件函数传递参数了,则对应的事件函数形参接收的则是传递过来的数据

    1
    2
    3
    4
    5
    6
    7
    8
    // template
    <button @click="tapHandle(1)">点我啊</button>
    // script
    methods: {
    tapHandle (num) {
    console.log(num)
    }
    }
  • 如果获取事件对象也想传递参数

    1
    2
    3
    4
    5
    6
    7
    8
    // template
    <button @click="tapHandle(1,$event)">点我啊</button>
    // script
    methods: {
    tapHandle (num,e) {
    console.log(num,e)
    }
    }

uni的生命周期

应用的生命周期

生命周期的概念:一个对象从创建、运行、销毁的整个过程被成为生命周期。

生命周期函数:在生命周期中每个阶段会伴随着每一个函数的触发,这些函数被称为生命周期函数

uni-app 支持如下应用生命周期函数:

函数名 说明
onLaunch 当uni-app 初始化完成时触发(全局只触发一次)
onShow 当 uni-app 启动,或从后台进入前台显示
onHide 当 uni-app 从前台进入后台
onError 当 uni-app 报错时触发
页面的生命周期

uni-app 支持如下页面生命周期函数:

函数名 说明 平台差异说明 最低版本
onLoad 监听页面加载,其参数为上个页面传递的数据,参数类型为Object(用于页面传参),参考示例
onShow 监听页面显示。页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面
onReady 监听页面初次渲染完成。
onHide 监听页面隐藏
onUnload 监听页面卸载

下拉刷新

开启下拉刷新

在uni-app中有两种方式开启下拉刷新

  • 需要在 pages.json 里,找到的当前页面的pages节点,并在 style 选项中开启 enablePullDownRefresh
  • 通过调用uni.startPullDownRefresh方法来开启下拉刷新
通过配置文件开启

创建list页面进行演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<view>
杭州学科
<view v-for="(item,index) in arr" :key="index">
{{item}}
</view>
</view>
</template>

<script>
export default {
data () {
return {
arr: ['前端','java','ui','大数据']
}
}
}
</script>

<style>
</style>

通过pages.json文件中找到当前页面的pages节点,并在 style 选项中开启 enablePullDownRefresh

1
2
3
4
5
6
{
"path":"pages/list/list",
"style":{
"enablePullDownRefresh": true
}
}
通过API开启

api文档

1
uni.startPullDownRefresh()
监听下拉刷新

通过onPullDownRefresh可以监听到下拉刷新的动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
data () {
return {
arr: ['前端','java','ui','大数据']
}
},
methods: {
startPull () {
uni.startPullDownRefresh()
}
},
onPullDownRefresh () {
console.log('触发下拉刷新了')
}
}
关闭下拉刷新

uni.stopPullDownRefresh()

停止当前页面下拉刷新。

案例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<view>
<button type="primary" @click="startPull">开启下拉刷新</button>
杭州学科
<view v-for="(item,index) in arr" :key="index">
{{item}}
</view>
</view>
</template>
<script>
export default {
data () {
return {
arr: ['前端','java','ui','大数据']
}
},
methods: {
startPull () {
uni.startPullDownRefresh()
}
},

onPullDownRefresh () {
this.arr = []
setTimeout(()=> {
this.arr = ['前端','java','ui','大数据']
uni.stopPullDownRefresh()
}, 1000);
}
}
</script>

上拉加载

通过在pages.json文件中找到当前页面的pages节点下style中配置onReachBottomDistance可以设置距离底部开启加载的距离,默认为50px

通过onReachBottom监听到触底的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<view>
<button type="primary" @click="startPull">开启下拉刷新</button>
杭州学科
<view v-for="(item,index) in arr" :key="index">
{{item}}
</view>
</view>
</template>
<script>
export default {
data () {
return {
arr: ['前端','java','ui','大数据','前端','java','ui','大数据']
}
},
onReachBottom () {
console.log('触底了')
}
}
</script>

<style>
view{
height: 100px;
line-height: 100px;
}
</style>

网络请求

在uni中可以调用uni.request方法进行请求网络请求

需要注意的是:在小程序中网络相关的 API 在使用前需要配置域名白名单。

发送get请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<view>
<button @click="sendGet">发送请求</button>
</view>
</template>
<script>
export default {
methods: {
sendGet () {
uni.request({
url: 'http://localhost:8082/api/getlunbo',
success(res) {
console.log(res)
}
})
}
}
}
</script>

发送post请求

数据缓存

uni.setStorage

官方文档

将数据存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个异步接口。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<view>
<button type="primary" @click="setStor">存储数据</button>
</view>
</template>

<script>
export default {
methods: {
setStor () {
uni.setStorage({
key: 'id',
data: 100,
success () {
console.log('存储成功')
}
})
}
}
}
</script>

<style>
</style>
uni.setStorageSync

将 data 存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个同步接口。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<view>
<button type="primary" @click="setStor">存储数据</button>
</view>
</template>

<script>
export default {
methods: {
setStor () {
uni.setStorageSync('id',100)
}
}
}
</script>

<style>
</style>
uni.getStorage

从本地缓存中异步获取指定 key 对应的内容。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<view>
<button type="primary" @click="getStorage">获取数据</button>
</view>
</template>
<script>
export default {
data () {
return {
id: ''
}
},
methods: {
getStorage () {
uni.getStorage({
key: 'id',
success: res=>{
this.id = res.data
}
})
}
}
}
</script>
uni.getStorageSync

从本地缓存中同步获取指定 key 对应的内容。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<view>
<button type="primary" @click="getStorage">获取数据</button>
</view>
</template>
<script>
export default {
methods: {
getStorage () {
const id = uni.getStorageSync('id')
console.log(id)
}
}
}
</script>
uni.removeStorage

从本地缓存中异步移除指定 key。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<view>
<button type="primary" @click="removeStorage">删除数据</button>
</view>
</template>
<script>
export default {
methods: {
removeStorage () {
uni.removeStorage({
key: 'id',
success: function () {
console.log('删除成功')
}
})
}
}
}
</script>
uni.removeStorageSync

从本地缓存中同步移除指定 key。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<view>
<button type="primary" @click="removeStorage">删除数据</button>
</view>
</template>
<script>
export default {
methods: {
removeStorage () {
uni.removeStorageSync('id')
}
}
}
</script>

上传图片、预览图片

上传图片

uni.chooseImage方法从本地相册选择图片或使用相机拍照。

案例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<view>
<button @click="chooseImg" type="primary">上传图片</button>
<view>
<image v-for="item in imgArr" :src="item" :key="index"></image>
</view>
</view>
</template>

<script>
export default {
data () {
return {
imgArr: []
}
},
methods: {
chooseImg () {
uni.chooseImage({
count: 9,
success: res=>{
this.imgArr = res.tempFilePaths
}
})
}
}
}
</script>
预览图片

结构

1
2
3
<view>
<image v-for="item in imgArr" :src="item" @click="previewImg(item)" :key="item"></image>
</view>

预览图片的方法

1
2
3
4
5
6
previewImg (current) {
uni.previewImage({
urls: this.imgArr,
current
})
}

条件注释实现跨段兼容

条件编译是用特殊的注释作为标记,在编译时根据这些特殊的注释,将注释里面的代码编译到不同平台。

写法:以 #ifdef 加平台标识 开头,以 #endif 结尾。

平台标识

值 平台 参考文档
APP-PLUS 5+App HTML5+ 规范
H5 H5
MP-WEIXIN 微信小程序 微信小程序
MP-ALIPAY 支付宝小程序 支付宝小程序
MP-BAIDU 百度小程序 百度小程序
MP-TOUTIAO 头条小程序 头条小程序
MP-QQ QQ小程序 (目前仅cli版支持)
MP 微信小程序/支付宝小程序/百度小程序/头条小程序/QQ小程序
组件的条件注释

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- #ifdef H5 -->
<view>
h5页面会显示
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view>
微信小程序会显示
</view>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<view>
app会显示
</view>
<!-- #endif -->
api的条件注释

代码演示

1
2
3
4
5
6
7
8
onLoad () {
//#ifdef MP-WEIXIN
console.log('微信小程序')
//#endif
//#ifdef H5
console.log('h5页面')
//#endif
}

样式的条件注释

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* #ifdef H5 */
view{
height: 100px;
line-height: 100px;
background: red;
}
/* #endif */
/* #ifdef MP-WEIXIN */
view{
height: 100px;
line-height: 100px;
background: green;
}
/* #endif */

uni中的导航跳转

利用navigator进行跳转

navigator详细文档:文档地址

跳转到普通页面

1
2
3
<navigator url="/pages/about/about" hover-class="navigator-hover">
<button type="default">跳转到关于页面</button>
</navigator>

跳转到tabbar页面

1
2
3
<navigator url="/pages/message/message" open-type="switchTab">
<button type="default">跳转到message页面</button>
</navigator>
利用编程式导航进行跳转

导航跳转文档)

利用navigateTo进行导航跳转

保留当前页面,跳转到应用内的某个页面,使用uni.navigateBack可以返回到原页面。

1
<button type="primary" @click="goAbout">跳转到关于页面</button>

通过navigateTo方法进行跳转到普通页面

1
2
3
4
5
goAbout () {
uni.navigateTo({
url: '/pages/about/about',
})
}

通过switchTab跳转到tabbar页面

跳转到tabbar页面

1
<button type="primary" @click="goMessage">跳转到message页面</button>

通过switchTab方法进行跳转

1
2
3
4
5
goMessage () {
uni.switchTab({
url: '/pages/message/message'
})
}

redirectTo进行跳转

关闭当前页面,跳转到应用内的某个页面。

1
2
3
4
5
6
7
8
<!-- template -->
<button type="primary" @click="goMessage">跳转到message页面</button>
<!-- js -->
goMessage () {
uni.switchTab({
url: '/pages/message/message'
})
}

通过onUnload测试当前组件确实卸载

1
2
3
onUnload () {
console.log('组件卸载了')
}
导航跳转传递参数

在导航进行跳转到下一个页面的同时,可以给下一个页面传递相应的参数,接收参数的页面可以通过onLoad生命周期进行接收

传递参数的页面

1
2
3
4
5
goAbout () {
uni.navigateTo({
url: '/pages/about/about?id=80',
});
}

接收参数的页面

1
2
3
4
5
6
7
<script>
export default {
onLoad (options) {
console.log(options)
}
}
</script>

####

uni-app中组件的创建

在uni-app中,可以通过创建一个后缀名为vue的文件,即创建一个组件成功,其他组件可以将该组件通过impot的方式导入,在通过components进行注册即可

  • 创建login组件,在component中创建login目录,然后新建login.vue文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <template>
    <view>
    这是一个自定义组件
    </view>
    </template>

    <script>
    </script>

    <style>
    </style>
  • 在其他组件中导入该组件并注册

    1
    import login from "@/components/test/test.vue"
  • 注册组件

    1
    components: {test}
  • 使用组件

    1
    <test></test>
组件的生命周期函数
beforeCreate 在实例初始化之后被调用。详见
created 在实例创建完成后被立即调用。详见
beforeMount 在挂载开始之前被调用。详见
mounted 挂载到实例上去之后调用。详见 注意:此处并不能确定子组件被全部挂载,如果需要子组件完全挂载之后在执行操作可以使用$nextTickVue官方文档
beforeUpdate 数据更新时调用,发生在虚拟 DOM 打补丁之前。详见 仅H5平台支持
updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。详见 仅H5平台支持
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。详见
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。详见

组件的通讯

父组件给子组件传值

通过props来接受外界传递到组件内部的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<view>
这是一个自定义组件 {{msg}}
</view>
</template>

<script>
export default {
props: ['msg']
}
</script>

<style>
</style>

其他组件在使用login组件的时候传递值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<view>
<test :msg="msg"></test>
</view>
</template>

<script>
import test from "@/components/test/test.vue"
export default {
data () {
return {
msg: 'hello'
}
},

components: {test}
}
</script>
子组件给父组件传值

通过$emit触发事件进行传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<view>
这是一个自定义组件 {{msg}}
<button type="primary" @click="sendMsg">给父组件传值</button>
</view>
</template>

<script>
export default {
data () {
return {
status: '打篮球'
}
},
props: {
msg: {
type: String,
value: ''
}
},
methods: {
sendMsg () {
this.$emit('myEvent',this.status)
}
}
}
</script>

父组件定义自定义事件并接收参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<view>
<test :msg="msg" @myEvent="getMsg"></test>
</view>
</template>
<script>
import test from "@/components/test/test.vue"
export default {
data () {
return {
msg: 'hello'
}
},
methods: {
getMsg (res) {
console.log(res)
}
},

components: {test}
}
</script>
兄弟组件通讯

uni-ui的使用

uni-ui文档

1、进入Grid宫格组件

2、使用HBuilderX导入该组件

3、导入该组件

1
2
import uniGrid from "@/components/uni-grid/uni-grid.vue"
import uniGridItem from "@/components/uni-grid-item/uni-grid-item.vue"

4、注册组件

1
components: {uniGrid,uniGridItem}

5、使用组件

1
2
3
4
5
6
7
8
9
10
11
<uni-grid :column="3">
<uni-grid-item>
<text class="text">文本</text>
</uni-grid-item>
<uni-grid-item>
<text class="text">文本</text>
</uni-grid-item>
<uni-grid-item>
<text class="text">文本</text>
</uni-grid-item>
</uni-grid>

模拟器

  1. adb kill-server //结束adb服务
  2. adb start-server //启动adb服务
  3. adb devices //获取adb设备列表

win10学linux

发表于 2020-02-16 | 分类于 java

Linux环境

win10家庭版自带虚拟机Hyper-V的设置

一般win10家庭版并不带Hyper-V虚拟机,解决方法:

新建Hyper-V.bat文件,内容为

pushd "%~dp0"

dir /b %SystemRoot%\servicing\Packages\*Hyper-V*.mum >hyper-v.txt

for /f %%i in ('findstr /i . hyper-v.txt 2^>nul') do dism /online /norestart /add-package:"%SystemRoot%\servicing\Packages\%%i"

del hyper-v.txt

Dism /online /enable-feature /featurename:Microsoft-Hyper-V-All /LimitAccess /ALL

右键-以管理员身份运行,这时就会自动的安装虚拟机功能。

下载linux镜像

下载linux镜像,运行Hyper-V管理器,并加载linux镜像

nginx

发表于 2020-02-13 | 分类于 nginx

nginx介绍

Nginx(“engine x”)是一款是由俄罗斯的程序设计师Igor Sysoev所开发高性能的 Web和 反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。

在高连接并发的情况下,Nginx是Apache服务器不错的替代品。

Nginx 安装

下载nginx

下载地址:http://nginx.org/download/nginx-1.6.2.tar.gz
[root@bogon src]# cd /usr/local/src/
[root@bogon src]# wget http://nginx.org/download/nginx-1.6.2.tar.gz

解压安装包

[root@bogon src]# tar zxvf nginx-1.6.2.tar.gz

编译安装

[root@bogon nginx-1.6.2]# ./configure --prefix=/usr/local/webserver/nginx --with-http_stub_status_module --with-http_ssl_module --with-pcre=/usr/local/src/pcre-8.35
[root@bogon nginx-1.6.2]# make
[root@bogon nginx-1.6.2]# make install

查看nginx版本

[root@bogon nginx-1.6.2]# /usr/local/webserver/nginx/sbin/nginx -v

Nginx 配置

创建 Nginx 运行使用的用户 www

[root@bogon conf]# /usr/sbin/groupadd www 
[root@bogon conf]# /usr/sbin/useradd -g www www

配置nginx.conf ,将/usr/local/webserver/nginx/conf/nginx.conf替换为以下内容

[root@bogon conf]#  cat /usr/local/webserver/nginx/conf/nginx.conf

user www www;
worker_processes 2; #设置值和CPU核心数一致
error_log /usr/local/webserver/nginx/logs/nginx_error.log crit; #日志位置和日志级别
pid /usr/local/webserver/nginx/nginx.pid;
#Specifies the value for maximum file descriptors that can be opened by this process.
worker_rlimit_nofile 65535;
events
{
  use epoll;
  worker_connections 65535;
}
http
{
  include mime.types;
  default_type application/octet-stream;
  log_format main  '$remote_addr - $remote_user [$time_local] "$request" '
               '$status $body_bytes_sent "$http_referer" '
               '"$http_user_agent" $http_x_forwarded_for';

#charset gb2312;

  server_names_hash_bucket_size 128;
  client_header_buffer_size 32k;
  large_client_header_buffers 4 32k;
  client_max_body_size 8m;

  sendfile on;
  tcp_nopush on;
  keepalive_timeout 60;
  tcp_nodelay on;
  fastcgi_connect_timeout 300;
  fastcgi_send_timeout 300;
  fastcgi_read_timeout 300;
  fastcgi_buffer_size 64k;
  fastcgi_buffers 4 64k;
  fastcgi_busy_buffers_size 128k;
  fastcgi_temp_file_write_size 128k;
  gzip on; 
  gzip_min_length 1k;
  gzip_buffers 4 16k;
  gzip_http_version 1.0;
  gzip_comp_level 2;
  gzip_types text/plain application/x-javascript text/css application/xml;
  gzip_vary on;

  #limit_zone crawler $binary_remote_addr 10m;
 #下面是server虚拟主机的配置
 server
  {
    listen 80;#监听端口
    server_name localhost;#域名
    index index.html index.htm index.php;
    root /usr/local/webserver/nginx/html;#站点目录
      location ~ .*\.(php|php5)?$
    {
      #fastcgi_pass unix:/tmp/php-cgi.sock;
      fastcgi_pass 127.0.0.1:9000;
      fastcgi_index index.php;
      include fastcgi.conf;
    }
    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|ico)$
    {
      expires 30d;
  # access_log off;
    }
    location ~ .*\.(js|css)?$
    {
      expires 15d;
   # access_log off;
    }
    access_log off;
  }

}

检查配置文件nginx.conf的正确性命令

[root@bogon conf]# /usr/local/webserver/nginx/sbin/nginx -t

启动 Nginx

[root@bogon conf]# /usr/local/webserver/nginx/sbin/nginx

Nginx 其他命令

/usr/local/webserver/nginx/sbin/nginx -s reload            # 重新载入配置文件
/usr/local/webserver/nginx/sbin/nginx -s reopen            # 重启 Nginx
/usr/local/webserver/nginx/sbin/nginx -s stop              # 停止 Nginx

详细配置

#user  nobody; #配置用户或者组,默认为nobody nobody。
worker_processes  auto; #number | auto;默认为1,最好配置成auto,自动匹配进程数

 #制定日志路径,级别。这个设置可以放入全局块,http块,server块,级别以此为:debug|info|notice|warn|error|crit|alert|emerg
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid; #指定nginx进程运行文件存放地址


events {
    worker_connections  1024; #最大连接数,默认为512
}


http {
    include       mime.types; #文件扩展名与文件类型映射表
    default_type  application/octet-stream;  #默认文件类型,默认为text/plain

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on; #允许sendfile方式传输文件,默认为off
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65; #连接超时时间,默认为75s

    #gzip  on; #启用Gizp压缩

    #include /usr/local/nginx/conf/vhost/*;

    server {
            listen       80;
            server_name  #监听地址;

            #charset koi8-r;

            #access_log  logs/host.access.log  main;

            location / { #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。
               proxy_pass http://mysvr;  #请求转向mysvr 定义的服务器列表
               #设置禁止浏览器缓存,每次都从服务器请求
               add_header Cache-Control no-cache;
               add_header Cache-Control private;#表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),可以缓存响应内容。响应只作为私有的缓存,不能在用户间共享。如果要求HTTP认证,响应会自动设置为private。
            deny 127.0.0.1;  #拒绝的ip
               allow 172.18.5.54; #允许的ip
            }

            location /material/ {
               proxy_pass http://mysvr;  #请求转向mysvr 定义的服务器列表
               #设置禁止浏览器缓存,每次都从服务器请求
               add_header Cache-Control no-cache;
               add_header Cache-Control private;#表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),可以缓存响应内容。响应只作为私有的缓存,不能在用户间共享。如果要求HTTP认证,响应会自动设置为private。
            }
     }

     server {
            listen       80;
            server_name  #监听地址;

            #charset koi8-r;

            #access_log  logs/host.access.log  main;

            location / { #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。
               proxy_pass http://mysvr;  #请求转向mysvr 定义的服务器列表
               #设置禁止浏览器缓存,每次都从服务器请求
               add_header Cache-Control no-cache;
               add_header Cache-Control private;#表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),可以缓存响应内容。响应只作为私有的缓存,不能在用户间共享。如果要求HTTP认证,响应会自动设置为private。
            deny 127.0.0.1;  #拒绝的ip
               allow 172.18.5.54; #允许的ip
            }

            location /material/ {
               proxy_pass http://mysvr;  #请求转向mysvr 定义的服务器列表
               #设置禁止浏览器缓存,每次都从服务器请求
               add_header Cache-Control no-cache;
               add_header Cache-Control private;#表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),可以缓存响应内容。响应只作为私有的缓存,不能在用户间共享。如果要求HTTP认证,响应会自动设置为private。
            }
     }


    #server {
    #    listen       80;
    #    server_name  xxx.com;
    #
    #    #charset koi8-r;
    #
    #    #access_log  logs/host.access.log  main;
    #
    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #
    #    #error_page  404              /404.html;
    #
    #    # redirect server error pages to the static page /50x.html
    #    #
    #    error_page   500 502 503 504  /50x.html;
    #    location = /50x.html {
    #        root   html;
    #    }
    #
    #    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #    #
    #    #location ~ \.php$ {
    #    #    proxy_pass   http://127.0.0.1;
    #    #}
    #
    #    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #    #
    #    #location ~ \.php$ {
    #    #    root           html;
    #    #    fastcgi_pass   127.0.0.1:9000;
    #    #    fastcgi_index  index.php;
    #    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    #    include        fastcgi_params;
    #    #}
    #
    #    # deny access to .htaccess files, if Apache's document root
    #    # concurs with nginx's one
    #    #
    #    #location ~ /\.ht {
    #    #    deny  all;
    #    #}
    #}


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

NGINX之反向代理与负载均衡

反向代理:

从图中,我们可以知道,对于浏览器来说,他会发一个http://www.a.com/uri请求到Nginx服务器,对于他来说,他认为数据就是从http://www.a.com/uri域中返回的,事实上,当http://www.a.com/uri到达Nginx服务器后,Nginx服务器会将其转发给http://www.b.com/uri,从http://www.b.com/uri域中取得数据并将其返回给浏览器,这个步骤浏览器是不知道的,也就是说,浏览器并不知道http://www.b.com/uri该域的存在,同理,http://www.b.com/uri所在的域(图中的Tomcat)也并不知道浏览器的存在,他也只对Nginx负责。Nginx的这么一个过程便称为反向代理。

那么,Nginx服务器是如何实现这一步的呢,事实上也很简单,只需要在location中做一下简单的配置即可,命令大概如下图所示:(配置完命令记得reload重新加载才能生效)

重点在于location处,这样的配置代表的是,所有来自浏览器的请求,在Nginx收到之后,都会代理到http://192.168.1.62:8080所在的地方

比如,我浏览器上发起http://192.168.1.61/a/index.html;Nginx收到之后,将会发出http:// 192.168.1.62:8080/a/index.html这么一个请求到所连接的服务器上,如上图的Tomcat。

接下来我们做这样一个假设,假如后端连接着几台。几十台服务器呢,这个时候Nginx也是做同样的代理吗,答案是肯定的。图示如下:那么,在这么多台服务器上,Nginx的转发又是基于怎样的策略呢?这个时候就涉及在负载均衡了,说白了就是,应该怎样的分发,才能做到资源的最大限度的利用?

负载均衡策略

(

我们这里假设三台服务器的IP地址分别为

http:// 192.168.1.62:8080

http:// 192.168.1.63:8080

http:// 192.168.1.64:8080

)

这里我们把后台所有的服务器放入upstream中,并在代理中进行引用。

其他的配置
备份与停机状态:
server 192.168.1.64 backup;//备份,不参与转发,只有当所有服务器都挂掉时才参与转发;

server 192.168.1.65 down;//临时停机维护,不参与任何转发,是关闭状态,

down存在的意义在于,有时我们需要对服务器做临时停机更新维护,假如我们直接关闭服务器的话,那么对于Nginx来说,他还是会把请求发到该服务器上的,因为他并不知道服务器已关,而设置down后,Nginx则不会再发到该服务器上了,避免造成无用的请求浪费。

max_fails: 达到指定次数后认为服务器挂掉

fail_timeout:挂掉多久后再次测试是否已经挂掉

配置命令

server 192.168.1.66 max_fails=2 fail_timeout=60s;

奇迹男孩

发表于 2020-02-11 | 分类于 电影台词

零

My mom always said…”if you don’t like where you are…just picture where you wanna be.”(by Auggie)

我妈妈常说,“如果你不喜欢这里,就想象一个你想去的地方。”(奥吉)

壹

“When given the choice between being right or being kind , choose kind.”(Mr Brown’s September Precet)

“如果要从正确和善良中,做出选择,请选择善良。”(布朗老师的九月箴言)

叁

Because I’m your mom , it counts the most because I know you the most.(By Isabel)

正因为我是你妈妈说话才算数,因为我最了解你。(伊莎贝尔)

肆

Because school sucks. And people change.So if you wanna be a normal kid,Auggie,the those are the rules.(By Via)

上学就是这么糟糕。人也是会变的。如果你想当普通小孩奥吉,这就是规则。(维娅)

伍

While nothing justifies striking another student…I know goos friends are worth defending.(By Mr. Tushman)

虽然殴打同学的行为不能通融,但我知道友情是值得捍卫的。(图始曼先生)

六

Maybe if we knew what other people were thinking, we’d know that no one’s ordinary.And we all deserve a standing ovation at least once in our lives.(By Auggie)

如果我们了解别人的想法,就会知道,没有人是普通的。每个人都值得大家站起来为他鼓掌一次。

柒

Be kind,for everyone is fighting a hard battle.And if you really wanna see what people are… all you have to do ..is look.(By Auggie)

善良一点,因为每个人都在与人生苦战。如果你想真正了解他人,你只需要用心…去看。(奥吉)

圣经解惑

发表于 2019-12-14 | 分类于 信仰

约拿的神迹

路加福音11章29节中讲道“这世代是一个邪恶的世代。他们求看神迹,除了约拿的神迹外,再没有神迹给他们看了。约拿怎样为尼尼微人成了神迹,人子也要照这样为这世代的人成为神迹。”

约拿的神迹是什么?约拿的神迹就是悔改的神迹。尼尼微人因为悔改,而是神改变了要毁灭他们的计划。照样耶稣来世界上就是要让人悔改归向上帝,而脱离灭亡。尼尼微城的最终毁灭,是在约拿之后的几百年后,照样上帝也在宽容和等待世人的悔改。

1…3456
Airthink

Airthink

The Pursuit of Happyness

58 日志
20 分类
24 标签
GitHub
© 2018 - 2022 Airthink