技术标签: golang 数据库+NoSql 数据库 mongodb
作者: sdghchj
本文链接:golang操作mongodb的驱动mongo-go-driver的事务支持和访问控制_sdghchj的博客-程序员秘密_golang mongodb 事务
交叉阅读:MongoDB 4.0 正式版轰动发布,功能越来越强大,支持多文档事务
mongodb要支持事务,需要满足以下条件:
下载地址 目前最新的release版本是4.0.5,package 类型是server:
我用的是ubuntu 16.04系统 x64,所以我直接下载该该平台的server deb包进行安装
[email protected]:~$ wget https://repo.mongodb.org/apt/ubuntu/dists/xenial/mongodb-org/4.0/multiverse/binary-amd64/mongodb-org-server_4.0.5_amd64.deb
[email protected]::~$ dkpg -i mongodb-org-server_4.0.5_amd64.deb
[email protected]:~$ whereis mongod #可以看到安装后的程序和配置文件路径
mongod: /usr/bin/mongod /etc/mongod.conf /usr/share/man/man1/mongod.1.gz
而要在replication set模式下同时支持访问控制,那么多节点之间也需要进行内部认证,有几种方式,即设置配置文件中的security.clusterAuthMode,这里我使用其默认的keyFile方式,所以需要事先准备一个保存有密钥的keyFile文件,要求:
#用openssl命令随机生成一个64位(可自己定)长度的密码串,写到mongod-keyfile中,并修改权限
[email protected]:~$ openssl rand -base64 64 > ./mongod-keyfile
[email protected]:~$ sudo mv ./mongod-keyfile /etc
[email protected]:~$ sudo chmod 600 /etc/mongod-keyfile
所有mongod进程都要使用这个相同的密钥,所以在同一台主机上可以共享这个文件,如果是多台主机,最好将这个文件拷贝到每台机器上给mongod进程使用,保证相同密钥。
如果有条件,最好准备多台主机。我测试,只有一台主机,所以运行3个端口的进程。
打开mongod.conf配置文件,做点修改:
[email protected]:~$ sudo vi /etc/mongod.conf #看到文件内容如下
# mongod.conf
# for documentation of all options, see:
# http://docs.mongodb.org/manual/reference/configuration-options/
# Where and how to store data.
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
# engine:
# mmapv1:
# wiredTiger:
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
# network interfaces
net:
port: 27017 #端口
bindIp: 0.0.0.0 #地址默认是127.0.0.1 ,如果想要网络上能访问到,最好改成外网地址或全0
# how the process runs
processManagement:
timeZoneInfo: /usr/share/zoneinfo
security:
authorization: enabled #开启访问控制
#clusterAuthMode: keyFile #默认的
keyFile: /etc/mongod-keyfile #指定keyFile文件
#operationProfiling:
replication: #此行默认是加#注释掉的,需要去掉#,开启复本集模式,再加上下面一行
replSetName: rs1 #rs1是复本集的名字,可自定义,但集群中所有server的配置必须一样
#sharding:
## Enterprise-Only Options:
#auditLog:
#snmp:
修改配置文件后,复制两份:
[email protected]:~$ sudo cp /etc/mongod.conf /etc/mongod1.conf
[email protected]:~$ sudo cp /etc/mongod.conf /etc/mongod2.conf
打开/etc/mongod1.conf,修改三行:
[email protected]:~$ sudo vi /etc/mongod1.conf
# mongod.conf
# for documentation of all options, see:
# http://docs.mongodb.org/manual/reference/configuration-options/
# Where and how to store data.
storage:
dbPath: /var/lib/mongodb1 #此处的数据存储路径目录改一下,以区分
journal:
enabled: true
# engine:
# mmapv1:
# wiredTiger:
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod1.log #此处的日志文件路径改一下,以区分
# network interfaces
net:
port: 27018 #因在同一主机上,需要一个不同的端口,改为27018
bindIp: 0.0.0.0
# how the process runs
processManagement:
timeZoneInfo: /usr/share/zoneinfo
security:
authorization: enabled
#clusterAuthMode: keyFile
keyFile: /etc/mongod-keyfile
#operationProfiling:
replication:
replSetName: rs1
#sharding:
## Enterprise-Only Options:
#auditLog:
#snmp:
同样修改/etc/mongod2.conf,对应修改即可,端口27019。
然后启动三个mongod服务进程:
[email protected]:~$ sudo mongod --fork -f /etc/mongod.conf #--fork表示后台进程运行
[email protected]:~$ sudo mongod --fork -f /etc/mongod1.conf
[email protected]:~$ sudo mongod --fork -f /etc/mongod2.conf
[email protected]:~$ ps -ef | grep mongod #可查看一下三个进程
下载地址 package 类型选 shell
[email protected]:~$ wget https://repo.mongodb.org/apt/ubuntu/dists/xenial/mongodb-org/4.0/multiverse/binary-amd64/mongodb-org-shell_4.0.5_amd64.deb
[email protected]::~$ dkpg -i mongodb-org-shell_4.0.5_amd64.deb
[email protected]:~$ whereis mongo #可以看到安装后的程序和配置文件路径
mongo: /usr/bin/mongo /usr/share/man/man1/mongo.1.gz
然后需要用客户端连接一个server,并初始化复本集:
[email protected]:~$ mongo #运行mongo客户端,默认是连接127.0.0.1:27107/test,也可以指定服务器地址,比如 mongodb://127.0.0.1:27107/test,test表示数据库名。
MongoDB shell version v4.0.5
connecting to: mongodb://127.0.0.1:27017/?gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("76f42f17-4012-4ad0-b386-7e66459af51d") }
MongoDB server version: 4.0.5
Server has startup warnings:
2018-12-24T22:37:51.871+0800 I STORAGE [initandlisten]
2018-12-24T22:37:51.871+0800 I STORAGE [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine
2018-12-24T22:37:51.871+0800 I STORAGE [initandlisten] ** See http://dochub.mongodb.org/core/prodnotes-filesystem
2018-12-24T22:37:52.628+0800 I CONTROL [initandlisten]
2018-12-24T22:37:52.628+0800 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database.
2018-12-24T22:37:52.628+0800 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted.
2018-12-24T22:37:52.628+0800 I CONTROL [initandlisten] ** WARNING: You are running this process as the root user, which is not recommended.
2018-12-24T22:37:52.628+0800 I CONTROL [initandlisten]
---
Enable MongoDB's free cloud-based monitoring service, which will then receive and display
metrics about your deployment (disk utilization, CPU, operation statistics, etc).
The monitoring data will be available on a MongoDB website with a unique URL accessible to you
and anyone you share the URL with. MongoDB may use this information to make product
improvements and to suggest MongoDB products and deployment options to you.
To enable free monitoring, run the following command: db.enableFreeMonitoring()
To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
---
# [创建用户](https://docs.mongodb.com/manual/tutorial/enable-authentication/)
# 最开始是没有用户的,必须先去admin数据库中创建一个权限较高的管理员用户,然后用它来创建其它用户以及初始化replication set.否则很多操作都没有权限做了。
> use admin
switched to db admin
#如下创建admin用户,创建的用户信息也会自动同步到集群中所有server;
#userAdminAnyDatabase角色有权限管理其它数据库的用户
#readWriteAnyDabase角色有权限读写其它数据库
#clusterAdmin角色有权限操作replication set集群,比如执行rs.initiate
> db.createUser({user:'admin',pwd:'admin',roles:['userAdminAnyDatabase','readWriteAnyDatabase','clusterAdmin']})
successfully added user:{
user:'admin',
roles:['userAdminAnyDatabase','readWriteAnyDabase','clusterAdmin']
}
#先认证
>db.auth('admin','admin')
1
# 认证后可以使用show roles查看当前数据库有哪些角色可分配,还可以用show users显示当前数据库所有用户
# 如果没有开启访问控制,则执行show roles和show users等命令是不需要先认证的。
#切换到test测试数据库,创建一个dbOwner角色的用户,对当前的test数据库具有最高权限,是readWrite、dbAdmin和userAdmin三种角色的组合。
>use test
>db.createUser({user:'test',pwd:'123456',roles:['dbOwner']})
successfully added user:{
user:'test',
roles:['dbOwner']
}
#执行如下命令初始化集群的三个成员,rs1是集群名;members里的_id是序号,可自定义,不同就行;host是ip:端口,我用的是本地127.0.0.1,如果要外网访问,请设置相应的外网ip。
> rs.initiate({_id:"rs1",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"}]})
#成功会就会开始推选primary,一般是客户端连的这个server.
rs1:PRIMARY> rs.status() #查看一下复本集的状态,显示如下
{
"set" : "rs1",
"date" : ISODate("2018-12-25T06:02:09.320Z"),
"myState" : 1,
"term" : NumberLong(1),
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"heartbeatIntervalMillis" : NumberLong(2000),
"optimes" : {
"lastCommittedOpTime" : {
"ts" : Timestamp(1545717724, 1),
"t" : NumberLong(1)
},
"readConcernMajorityOpTime" : {
"ts" : Timestamp(1545717724, 1),
"t" : NumberLong(1)
},
"appliedOpTime" : {
"ts" : Timestamp(1545717724, 1),
"t" : NumberLong(1)
},
"durableOpTime" : {
"ts" : Timestamp(1545717724, 1),
"t" : NumberLong(1)
}
},
"lastStableCheckpointTimestamp" : Timestamp(1545717714, 1),
"members" : [
{
"_id" : 0,
"name" : "127.0.0.1:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 55458,
"optime" : {
"ts" : Timestamp(1545717724, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2018-12-25T06:02:04Z"),
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1545662551, 1),
"electionDate" : ISODate("2018-12-24T14:42:31Z"),
"configVersion" : 1,
"self" : true,
"lastHeartbeatMessage" : ""
},
{
"_id" : 1,
"name" : "127.0.0.1:27018",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 55189,
"optime" : {
"ts" : Timestamp(1545717724, 1),
"t" : NumberLong(1)
},
"optimeDurable" : {
"ts" : Timestamp(1545717724, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2018-12-25T06:02:04Z"),
"optimeDurableDate" : ISODate("2018-12-25T06:02:04Z"),
"lastHeartbeat" : ISODate("2018-12-25T06:02:07.796Z"),
"lastHeartbeatRecv" : ISODate("2018-12-25T06:02:07.795Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "",
"syncingTo" : "127.0.0.1:27017",
"syncSourceHost" : "127.0.0.1:27017",
"syncSourceId" : 0,
"infoMessage" : "",
"configVersion" : 1
},
{
"_id" : 2,
"name" : "127.0.0.1:27019",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 55189,
"optime" : {
"ts" : Timestamp(1545717724, 1),
"t" : NumberLong(1)
},
"optimeDurable" : {
"ts" : Timestamp(1545717724, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2018-12-25T06:02:04Z"),
"optimeDurableDate" : ISODate("2018-12-25T06:02:04Z"),
"lastHeartbeat" : ISODate("2018-12-25T06:02:07.796Z"),
"lastHeartbeatRecv" : ISODate("2018-12-25T06:02:07.795Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "",
"syncingTo" : "127.0.0.1:27017",
"syncSourceHost" : "127.0.0.1:27017",
"syncSourceId" : 0,
"infoMessage" : "",
"configVersion" : 1
}
],
"ok" : 1,
"operationTime" : Timestamp(1545717724, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1545717724, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
可以确定一下server的存储引擎是不是wiredTiger:
[email protected]:~$ echo "db.serverStatus()" | mongo | grep wiredTiger
"name" : "wiredTiger",
"wiredTiger" : {
如果没有输出,则不是wiredTiger,再看看具体是什么。因为输出内容较多,在终端上很难找,可以输出到文件去后再找storageEngine:
[email protected]:~$ echo "db.serverStatus()"| mongo > a.log
[email protected]:~$ vi a.log
本来mongod默认是wiredTiger,我就遇到一个问题:之前装过旧版本的mongod,其存储引擎是mmapv1 ,在dbpath路径下留下了数据文件,换成4.0的mongod server后,依然设置了该数据目录,导致它去读了旧的数据文件,就初始化成了mmapv1 引擎,最后测试事务的时候总报错不支持“transaction numbers”,需要支持文档级别锁的存储引擎。
在gopath项目里使用go get或者go vendor下载github.com\mongodb\mongo-go-driver开源包,具体过程略。
mongo-go-driver是mongo官方的golang驱动库,目前还有频繁修改。
驱动源码里,连接server过程内会先生成连接池,然后返回有一个client对象,通过client对象可以对server里的数据库集合进行读写。但是任何读写操作本身是不带session对象的,所以在操作前会先生成一个默认的session对象,然后再从连接池中取一个连接来进行通信。而事务相关的接口是在session接口内,包括Transaction的Start、Abort、Commit,但session接口里并没有其它CRUD相关方法。
研究时走了些弯路,先看一下如下代码:
import (
"context"
"github.com/mongodb/mongo-go-driver/mongo"
"net/url"
"fmt"
)
func main(){
connectString := "mongodb://127.0.0.1/test"
dbUrl, err := url.Parse(connectString)
if err != nil {
fmt.Println(err)
return
}
//认证参数设置,否则连不上
opts := &options.ClientOptions{}
opts.SetAuth(options.Credential{
AuthMechanism:"SCRAM-SHA-1",
AuthSource:"test",
Username:"test",
Password:"123456"})
client, err = mongo.Connect(context.Background(), connectString,opts)
if err != nil {
fmt.Println(err)
return
}
db := client.Database(dbUrl.Path[1:])
ctx := context.Background()
defer db.Client().Disconnect(ctx)
col := db.Collection("test")
//先在事务外写一条id为“111”的记录
_,err = col.InsertOne(ctx, bson.M{"_id": "111", "name": "ddd", "age": 50})
if(err != nil){
fmt.Println(err)
return
}
session,err := db.Client().StartSession()
if(err != nil){
fmt.Println(err)
return
}
defer db.Client().EndSession()
//开始事务
err := session.StartTransaction()
if(err != nil){
fmt.Println(err)
return
}
//在事务内写一条id为“222”的记录
_, err = col.InsertOne(ctx, bson.M{"_id": "222", "name": "ddd", "age": 50})
if(err != nil){
fmt.Println(err)
return
}
//写重复id
_, err = col.InsertOne(ctx, bson.M{"_id": "111", "name": "ddd", "age": 50})
if err != nil {
session.AbortTransaction(ctx)
}else {
session.CommitTransaction(ctx)
}
}
//最终成功写入的数据有"111","222"两条,显然,理所当然认为的后两条数据并没有在事务内。
为什么读写操作没有在事务内?找任何一个操作源码看看,比如InsertOne:
//cellection.go
// InsertOne inserts a single document into the collection.
func (coll *Collection) InsertOne(ctx context.Context, document interface{},
opts ...*options.InsertOneOptions) (*InsertOneResult, error) {
if ctx == nil {
ctx = context.Background()
}
doc, err := transformDocument(coll.registry, document)
if err != nil {
return nil, err
}
doc, insertedID := ensureID(doc)
//下句很重要,从Context中获取的session
sess := sessionFromContext(ctx)
err = coll.client.ValidSession(sess)
if err != nil {
return nil, err
}
wc := coll.writeConcern
if sess != nil && sess.TransactionRunning() {
wc = nil
}
oldns := coll.namespace()
cmd := command.Insert{
NS: command.Namespace{DB: oldns.DB, Collection: oldns.Collection},
Docs: []bsonx.Doc{doc},
WriteConcern: wc,
Session: sess,
Clock: coll.client.clock,
}
// convert to InsertManyOptions so these can be argued to dispatch.Insert
insertOpts := make([]*options.InsertManyOptions, len(opts))
for i, opt := range opts {
insertOpts[i] = options.InsertMany()
insertOpts[i].BypassDocumentValidation = opt.BypassDocumentValidation
}
res, err := driver.Insert(
ctx, cmd,
coll.client.topology,
coll.writeSelector,
coll.client.id,
coll.client.topology.SessionPool,
coll.client.retryWrites,
insertOpts...,
)
rr, err := processWriteError(res.WriteConcernError, res.WriteErrors, err)
if rr&rrOne == 0 {
return nil, err
}
return &InsertOneResult{InsertedID: insertedID}, err
}
//session.go
// sessionFromContext checks for a sessionImpl in the argued context and returns the session if it
// exists
func sessionFromContext(ctx context.Context) *session.Client {
//从带key-Value的Context里取出session
s := ctx.Value(sessionKey{})
if ses, ok := s.(*sessionImpl); ses != nil && ok {
return ses.Client
}
return nil
}
// driver/insert.go
func Insert(
ctx context.Context,
cmd command.Insert,
topo *topology.Topology,
selector description.ServerSelector,
clientID uuid.UUID,
pool *session.Pool,
retryWrite bool,
opts ...*options.InsertManyOptions,
) (result.Insert, error) {
ss, err := topo.SelectServer(ctx, selector)
if err != nil {
return result.Insert{}, err
}
//Session为nil,则新建一个默认的session
// If no explicit session and deployment supports sessions, start implicit session.
if cmd.Session == nil && topo.SupportsSessions() {
cmd.Session, err = session.NewClientSession(pool, clientID, session.Implicit)
if err != nil {
return result.Insert{}, err
}
defer cmd.Session.EndSession()
}
insertOpts := options.MergeInsertManyOptions(opts...)
if insertOpts.BypassDocumentValidation != nil && ss.Description().WireVersion.Includes(4) {
cmd.Opts = append(cmd.Opts, bsonx.Elem{"bypassDocumentValidation", bsonx.Boolean(*insertOpts.BypassDocumentValidation)})
}
if insertOpts.Ordered != nil {
cmd.Opts = append(cmd.Opts, bsonx.Elem{"ordered", bsonx.Boolean(*insertOpts.Ordered)})
}
// Execute in a single trip if retry writes not supported, or retry not enabled
if !retrySupported(topo, ss.Description(), cmd.Session, cmd.WriteConcern) || !retryWrite {
if cmd.Session != nil {
cmd.Session.RetryWrite = false // explicitly set to false to prevent encoding transaction number
}
return insert(ctx, cmd, ss, nil)
}
// TODO figure out best place to put retry write. Command shouldn't have to know about this field.
cmd.Session.RetryWrite = retryWrite
cmd.Session.IncrementTxnNumber()
res, originalErr := insert(ctx, cmd, ss, nil)
// Retry if appropriate
if cerr, ok := originalErr.(command.Error); ok && cerr.Retryable() ||
res.WriteConcernError != nil && command.IsWriteConcernErrorRetryable(res.WriteConcernError) {
ss, err := topo.SelectServer(ctx, selector)
// Return original error if server selection fails or new server does not support retryable writes
if err != nil || !retrySupported(topo, ss.Description(), cmd.Session, cmd.WriteConcern) {
return res, originalErr
}
return insert(ctx, cmd, ss, cerr)
}
return res, originalErr
}
它这样设计,说明它希望在调用InsertOne里传入一个带session-value的context对象给它。问题关键是:sessionFromContext()方法还只认sessionKey{}这个key,而sessionKey struct是没有导出的,我们项目包里是访问不到,所以我们不能通过context.WithValue()方法建建一个session-value-context对象给它。
那么它包内肯定有构建session-value-context对象的,那就找sessionKey{}的出现位置,发现:
// UseSession creates a default session, that is only valid for the
// lifetime of the closure. No cleanup outside of closing the session
// is done upon exiting the closure. This means that an outstanding
// transaction will be aborted, even if the closure returns an error.
//
// If ctx already contains a mongo.Session, that mongo.Session will be
// replaced with the newly created mongo.Session.
//
// Errors returned from the closure are transparently returned from
// this method.
func (c *Client) UseSession(ctx context.Context, fn func(SessionContext) error) error {
return c.UseSessionWithOptions(ctx, options.Session(), fn)
}
// UseSessionWithOptions works like UseSession but allows the caller
// to specify the options used to create the session.
func (c *Client) UseSessionWithOptions(ctx context.Context, opts *options.SessionOptions, fn func(SessionContext) error) error {
defaultSess, err := c.StartSession(opts)
if err != nil {
return err
}
defer defaultSess.EndSession(ctx) //如果事务没有显示的Commit,内部会Abort。
//在这里了
sessCtx := sessionContext{
Context: context.WithValue(ctx, sessionKey{}, defaultSess),
Session: defaultSess,
}
return fn(sessCtx)
}
显然,只有如下closure闭包方式才能使用事务接口,修改golang测试代码如下:
import (
"context"
"github.com/mongodb/mongo-go-driver/mongo"
"net/url"
"fmt"
)
func main(){
connectString := "mongodb://127.0.0.1/test"
dbUrl, err := url.Parse(connectString)
if err != nil {
panic(err)
}
//认证参数设置,否则连不上
opts := &options.ClientOptions{}
opts.SetAuth(options.Credential{
AuthMechanism:"SCRAM-SHA-1",
AuthSource:"test",
Username:"test",
Password:"123456"})
client, err = mongo.Connect(context.Background(), connectString,opts)
if err != nil {
panic(err)
}
db := client.Database(dbUrl.Path[1:])
ctx := context.Background()
defer db.Client().Disconnect(ctx)
col := db.Collection("test")
//先在事务外写一条id为“111”的记录
_,err = col.InsertOne(ctx, bson.M{"_id": "111", "name": "ddd", "age": 50})
if(err != nil){
fmt.Println(err)
return
}
//第一个事务:成功执行
db.Client().UseSession(ctx, func(sessionContext mongo.SessionContext) error {
err = sessionContext.StartTransaction()
if(err != nil){
fmt.Println(err)
return err
}
//在事务内写一条id为“222”的记录
_, err = col.InsertOne(sessionContext, bson.M{"_id": "222", "name": "ddd", "age": 50})
if(err != nil){
fmt.Println(err)
return err
}
//在事务内写一条id为“333”的记录
_, err = col.InsertOne(sessionContext, bson.M{"_id": "333", "name": "ddd", "age": 50})
if err != nil {
sessionContext.AbortTransaction(sessionContext)
return err
}else {
sessionContext.CommitTransaction(sessionContext)
}
return nil
})
//第二个事务:执行失败,事务没提交,因最后插入了一条重复id "111",
err = db.Client().UseSession(ctx, func(sessionContext mongo.SessionContext) error {
err := sessionContext.StartTransaction()
if(err != nil){
fmt.Println(err)
return err
}
//在事务内写一条id为“222”的记录
_, err = col.InsertOne(sessionContext, bson.M{"_id": "444", "name": "ddd", "age": 50})
if(err != nil){
fmt.Println(err)
return err
}
//写重复id
_, err = col.InsertOne(sessionContext, bson.M{"_id": "111", "name": "ddd", "age": 50})
if err != nil {
sessionContext.AbortTransaction(sessionContext)
return err
}else {
sessionContext.CommitTransaction(sessionContext)
}
return nil
})
}
//最终数据只有 "111","222","333" 三条,事务测试成功。
本帖最后由 xz753692 于 2020-7-21 08:45 编辑我是一名跨考生,但是初试的专业课今年考了130,复试的成绩也算中上游,最终以初试第四,总成绩第五被拟录取。所以在初试专业课准备和复试的应对上有些经验想要分享在这里,希望能给各位备考生一些帮助。1.初试专业课我是从去年9月份左右才正式准备专业课内容的,这个时间其实已经晚于大多数的考研学生了,但由于C语言与数据结构这两门课都是非常基...
【重要】在CMD命令行下关闭进程的命令━━━━━━━━━━━━━━━━━━━━━━━━━━方法一:在"运行"中输入:ntsd -c q -pn 程序名字(在MS-Dos中的作用是一样的)方法二:ntsd使用以下参数杀死进程.c:\>ntsd -c q -p PID 只要你能提供进程的PID,那么你就可以干掉进程.法二:tskill命令
Toast:android studio提供的一种非常好的提醒方式,即提示信息,以短小信息的形式通知给用户,在一段时间之后会自动消失。下面我们通过一个示例进行解释它的用法示例:设置一个按钮,点击按钮的时候显示提示信息。步骤:第一步:在layout的文件中加入以下代码来设置一个按钮的属性<Button android:id="@+id/button_1"//为这个...
问题安装win下linux子系统,缺少组件比较多,安装组件时国外源速度太慢,更换为国内源;办法离阿里比较近,默认选择阿里源。执行以下指令sudo mv /etc/apt/sources.list /etc/apt/sources.list.bak #备份官方镜像源 vi /etc/apt/sources.list #新建镜像源文件i #进入编辑模式鼠标右键 #子系统界面继承win控制台的操作习惯,粘贴复制的内容<esc>:x #退出并保存阿里源:deb http://mi
自治区2011年一月普通高中学业水平考试试题卷新疆维吾尔自治区2011年1月普通高中学业水平考试试题卷信息技术第一部分 必修模块(共80分)(说明:全体考生必答题)一、单项选择题(本大题共20小题,每小题2分,共40分)阿依夏木拿了一张2005年版的喀什市的地图查找新开发区的某个地方,结果没找到。这说明信息具有A.时效性 B.价值性 C.真伪性 D.传递性2.机器人能够灭火、踢足球,主要...
Charles的下载和安装Charles 是一个网络抓包工具,相比 Fiddler,其功能更为强大,而且跨平台支持得更好,所以选用它来作为主要的移动端抓包工具。官方网站:https://www.charlesproxy.com下载链接:https://www.charlesproxy.com/download我们可以在官网下载最新的稳定版本,如图所示。可以发现,它支持 Windows、...
DispatcherServlet和ContextLoaderListenerDispatcherServlet的上下文仅仅是Spring MVC的上下文,而Spring加载的上下文是通过ContextLoaderListener来加载的。因此在/WEB-INF/[server-name]-servlet.xml中配置的Bean一般只针对Spring MVC有效,而在ContextLoader...
最近公司需求,循环查询后台日志,有新数据弹出提示,效果类似$notify的样式,然后踩了一些坑,这里记录下,帮助一些朋友。问题可以弹出,但是自定义的内容无法自定义事件。比如相加一个button按钮,却无法对这个button监听点击事件。后台查询并不会返回新增的日志,而是将所有的日志返回,需要前端对内容判断,确定哪些东西是新增,哪些是之前就有的日志。新增的就弹出,之前有的就不用动。解...
微信小程序开发交流qq群 173683895 承接微信小程序开发。扫码加微信。使用微信提供的APIwx.setTabBarItem(Object object)动态设置 tabBar 某一项的内容参数Object object属性 类型 默认值 必填 说明 index number 是 tabBar 的哪一项...
要想搞清楚一个文件的两个版本之间的不同之处并不是件简单的任务,而当该文件是比较长的源代码时,这就更加复杂了。 下面列出的工具可以帮助你分析和比较文档,必要时还可以进行合并。这些工具可以比较从Word文档到WAV文件等所有的文件类型,甚至支持代码语法高亮显示。有些是免费的,有些是收费的,适用于Mac OS X、Windows或Linux。 1. Beyond Compare
主要内容,一个Activity调用另一个Activity,并向另一个Activity传递消息。如果您对这块已经驾轻就熟,就请不要浪费您宝贵的时间看我闲扯了。如果您不太熟悉,希望您自己先试着通过查资料等方法写一下,然后再看。 昨天我们学习了一个Animation的示例,今天我想写一个简单一些的,并且把昨天的进行重构,组合到一起。如果你昨天没有读我的文章,那也没有关系,今天的是一个“中间加餐”,也就是说和之前没有联系。 今天的主题是Activity和Activity中值的传递。
RN入门-新建rn项目这里有个git源码 里面有每个demo的源码和说明,均可直接测试运行