相关文章推荐
热心的烈马  ·  php ...·  2 年前    · 
八块腹肌的葡萄酒  ·  C# ...·  2 年前    · 
define LUA_TTHREAD 8 变体(或者说子类型) /*** tags for Tagged Values have the following use of bits:* bits 0-3: actual tag (a LUA_T value)** bits 4-5: variant bits** bit 6: whether value is collectable*/ /*** LUA_TFUNCTION variants:** 0 - Lua function** 1 - light C function** 2 - regular C function (closure)*/ / Variant tags for functions / define LUA_TLCL (LUA_TFUNCTION | (0 << 4)) / Lua closure / define LUA_TLCF (LUA_TFUNCTION | (1 << 4)) / light C function / define LUA_TCCL (LUA_TFUNCTION | (2 << 4)) / C closure / / Variant tags for strings / define LUA_TSHRSTR (LUA_TSTRING | (0 << 4)) / short strings / define LUA_TLNGSTR (LUA_TSTRING | (1 << 4)) / long strings / / Variant tags for numbers / define LUA_TNUMFLT (LUA_TNUMBER | (0 << 4)) / float numbers / define LUA_TNUMINT (LUA_TNUMBER | (1 << 4)) / integer numbers / / Bit mark for collectable types / define BIT_ISCOLLECTABLE (1 << 6) lua中的对象都是用TValue来描述的,TValue中的tt_成员变量代表着这个TValue的类型。关于类型的具体定义,上面贴的代码中的注释中已经讲的比较清楚了。一个lua对象的类型是由一个7位的bits描述的。比如一个整数,这个对象的类型就是0011000(24)表示这个对象是数字类型中的整形,是一个不可回收对象。 C#如何获取lua对象和c语言和lua交互其实没啥本质区别,就是通过lua提供的c函数操作lua栈,直接从栈中取就可以了。区别在于如何把取到的值转换为c#认识的值。 如何在C#端描述这些类型简介lua的类型中boolean、string、number这几个类型是clr所认识的类型,所以clr就可以直接把这些类型拿过来用。具体就是直接调用Lua提供的lua_tonumber之类的c接口。lightUserData、table、function、userData、thread是C#不认识的类,需要通过某种标识(lua自带的reference系统)来表示。 boolean、string、number类这三个类上面已经说过了,直接用提供的接口转就可以,下面写几个需要注意的点: string虽然也是一个引用类型,但是clr在拿到这个string的指针时,还需要将这个string的数据直接复制进clr中才算转型结束(xlua也已经封装好了,不用我们自己去复制)。大部分类型转型失败的时候都不会报错,而是会返回一个默认值。就拿将一个lua对象转为int来说,最终是通过lua_tointegerx函数调用的,当lua对象不是number类型时,返回0:LUA_API lua_Integer lua_tointegerx (lua_State L, int idx, int pisnum) { lua_Integer res; const TValue *o = index2addr(L, idx); int isnum = tointeger(o, &res); if (!isnum) res = 0; /* call to 'tointeger' may change 'n' even if it fails */ if (pisnum) *pisnum = isnum; return res;}当一个number类型是浮点数时,转型整数不会进行取整操作,而是会直接返回0。因为lua默认对float转int的操作模式LUA_FLOORN2I是0,代表碰见float转int时返回0。/*** try to convert a value to an integer, rounding according to 'mode':** mode == 0: accepts only integral values** mode == 1: takes the floor of the number** mode == 2: takes the ceil of the number*/int luaV_tointeger (const TValue obj, lua_Integer p, int mode) { TValue v; again: if (ttisfloat(obj)) { lua_Number n = fltvalue(obj); lua_Number f = l_floor(n); if (n != f) { /* not an integral value? */ if (mode == 0) return 0; /* fails if mode demands integral value */ else if (mode > 1) /* needs ceil? */ f += 1; /* convert floor to ceil (remember: n != f) */ return lua_numbertointeger(f, p); } else if (ttisinteger(obj)) { *p = ivalue(obj); return 1; } else if (cvt2num(obj) && luaO_str2num(svalue(obj), &v) == vslen(obj) + 1) { obj = &v; goto again; /* convert result from 'luaO_str2num' to an integer */ } return 0; / conversion failed /}userDatauserData主要是lua对c#对象的引用,这里只简单说一下。代表c#对象的userData主要分两种。 把c#对象存在ObjectTranslator中,用下标作为引用(类似于lua中的reference)。经过GC优化的结构体和枚举,不存在ObjectTranslator中,而是把所有内容都打包到userdata中一起传入lua中。比如一个Vector3,那么xlua会把这个Vector3的x、y、z作为3个连续的float一起打包到userdata中。这样就避免了c#层的装箱、拆箱和gc操作。对table与function的引用简介这两个类型都是通过lua的reference系统来让c#持有对lua对象的引用。 lua reference系统c#就是通过这个系统来持有不认识的lua对象的。一共就两个接口: luaL_ref:把栈顶元素加入一个lua的表中,并返回下标。luaL_unref:把一个下标所代表元素从表中删除。这样就可以用一个整数来让lua外的环境持有这个lua对象。具体可以看下官方说明lua References luaBase类所有lua对象在c#中的基类,在初始化时通过luaL_ref生成lua对象的引用,在析构时通过luaL_unref移除引用。 对table的引用LuaTable一般情况下table在C#中被包装为LuaTable类,没啥特别的,只是在LuaBase的基础上增加了几个常用的函数。比如Get、Set之类的,让开发者可以避开一些不直观的栈操作。 Array、List、Dictionary这几个都差不多。都是把table中的key和value全部拿出来,组成Array或Dictionaray。 接口、其他类这两种转型是尝试把这个table看作对应的接口或类。比如将一个table转为IEnumberator就是把table转为SystemCollectionsIEnumeratorBridge类(继承了LuaBase、实现了IEnumerator的类,由Xlua生成),这个类实现了MoveNext和Reset。实现方法就是调用一下table中对应名称的函数。 对function的引用lua函数在c#中有两种表示: LuaFunctionLuaFunction和luaTable差不多,也是在LuaBase的基础上增加了几个常用函数,Call、Action之类的。 DelegateBridge为什么已经有LuaFunction还要一个DelegateBridge类?因为我们在c#中拿到一个lua函数时,大多数时候是要作为一个委托来时用的。DelegateBridge就是用来化简这个转型操作的。DelegateBridge的功能就是在持有lua函数引用的同时,将这个函数包装成各种各样的委托,让整个转型过程对开发人员无感知。下面是一个不使用DelegateBridge,自己转型的例子,比较繁琐: //将一个LuaFunction作为一个Action使用//其实LuaFunction.Cast就是干这个的,这里只是用简单的方式表达出来public static Action LuaFunctionToActionInt(XLua.LuaFunction luaFunction){ //由于luaFunction已经提供了Call操作封装了函数调用的各种栈操作,所以我们这里只需要用一个Action<int>把这个操作包装起来即可 return (x) => luaFunction.Call(x); public static void Test(){ XLua.LuaEnv luaEnv = new XLua.LuaEnv(); object[] rets = luaEnv.DoString("return function(x) CS.UnityEngine.Debug.LogError(\"print x: \"..x) end"); var luaFunction = (XLua.LuaFunction)rets[0]; Action<int> actionInt = LuaFunctionToActionInt(luaFunction); actionInt(10); }DelegateBridge重要成员 xlua在将lua函数转型的时候做了什么 Tips通过ObjectTranslator.getDelegateUsingGeneric生成委托时,会对返回值和参数进行不为值类型的约束。因为值类型在il2cpp下会有jit异常。这也是为什么我们发现有的委托类型不用注册也可以使用,但是有的就不行。在编辑器模式下,没有进行代码生成时,会通过Emit直接生成一个XLuaGenDelegateImplx类,内容和通过代码生成后的DelegateBridge一样,而不是全部通过反射来进行转型。让没有进行代码生成时的环境和真机环境更接近。DelegateBridge一般不会被直接引用,而是被bindto中的委托生成的闭包引用和被delegate_bridges作为弱引用持有。当一个DelegateBridge的bindto中的委托没有被任何对象引用时,这个DelegateBridge就会在下次gc时被gc掉。其他这里主要写了常用lua类型转型的简介和一些关键点。可能不够全面和细节。如果有什么错误或者问题可以在下面留言。 原文地址https://www.cnblogs.com/blueberryzzz/p/13066922.html

MongoDB 4.X CRUD基本操作 mangoDB目录 创建(Create Operations)db.collection.insert()db.collection.insertOne()db.collection.insertMany()关于返回确认信息查询(Read Operations)db.collection.find()db.collection.findOne()条件查询操作符更新(Update Operations)db.collection.update()db.collection.updateOne()db.collection.updateMany()删除(Delete Operations)db.collection.deleteOne()db.collection.deleteMany()总结参考正文 本文总结了MongoDB 4.X在mongo shell客户端涉及的对文档一些基本的增删改查操作,即CRUD操作。主要结合了自己平时使用MongoDB的操作命令,更详细的命令可以参考官方文档: https://docs.mongodb.com/manual/crud/ 。 回到顶部创建(Create Operations)创建(Create Operations)也叫插入操作,当集合不存在时,插入操作同时也会创建集合。MongoDB提供以下几种插入文档方法: db.collection.insert():在指定集合中插入单个或多个文档。db.collection.insertOne():在指定集合中插入单个文档(版本3.2新增)。db.collection.insertMany():在指定集合中插入多个文档(版本3.2新增)。回到顶部db.collection.insert()在平时的使用当中,db.collection.insert()是我用得最多的文档插入方式,具体的语法格式如下: db.collection.insert(, { writeConcern: &lt;document&gt;, ordered: &lt;boolean&gt; })参数说明: document:指定一个或多个文档;writeConcern:文档写入确认级别(可选),关于读写策略确认级别,以后再进行讨论;ordered:指定文档是否按顺序插入(可选),默认为true;当指定为true时,插入多个文档时将文档排序保存在一个数组中进行插入,如果其中有一个文档插入失败,则会导致数组中余下的文档不进行插入操作;当指定为false时,插入多个文档时将文档不进行排序保存在一个数组中进行插入,如果其中有一个文档插入失败,则不影响数组中余下的文档进行插入操作。如果插入的文档当中没有指定_id字段,则MongoDB会自动为文档生成具有唯一ObjectId值的字段_id。 使用示例: // 没有指定_id字段的插入单个文档db.products.insert( { item: "card", qty: 15 } ); // 指定_id字段的插入单个文档db.products.insert( { _id: 10, item: "box", qty: 20 } ); // 插入多个文档,不进行排序,多个文档包含在数组[]中db.products.insert( [ { _id: 11, item: "pencil", qty: 50, type: "no.2" }, { item: "pen", qty: 20 }, { item: "eraser", qty: 25 } // 插入多个文档,并进行排序db.products.insert( [ { _id: 20, item: "lamp", qty: 50, type: "desk" }, { _id: 21, item: "lamp", qty: 20, type: "floor" }, { _id: 22, item: "bulk", qty: 100 } ], { ordered: false }); 回到顶部db.collection.insertOne()语法格式如下: db.collection.insertOne(, { writeConcern: &lt;document&gt; })参数说明: 参考db.collection.insert()的参数说明。 使用示例: // 单行插入文档,关于_id字段指定与否也与db.collection.insert()一致db.products.insertOne( { item: "card", qty: 15 } );回到顶部db.collection.insertMany()语法格式如下: db.collection.insertMany( [ , , ... ], { writeConcern: &lt;document&gt;, ordered: &lt;boolean&gt; })参数说明: 参考db.collection.insert()的参数说明。 使用示例: 参考db.collection.insert()的参数说明。 回到顶部关于返回确认信息db.collection.insert()在插入文档成功之后返回的信息相对较为简洁: db.products.insert( { item: "card", qty: 15 } );WriteResult({ "nInserted" : 1, "writeConcernError" : [ ] })db.collection.insertOne()和db.collection.insertMany()返回的信息较为详细: db.products.insertOne( { item: "card", qty: 15 } );{ "acknowledged": true, "insertedId": ObjectId("5eccbd214139000074003be8") db.products.insertMany( [ { _id: 10, item: "large box", qty: 20 }, { _id: 11, item: "small box", qty: 55 }, { _id: 12, item: "medium box", qty: 30 } ] );{ "acknowledged": true, "insertedIds": [ }回到顶部查询(Read Operations)查询(Read Operations)读操作,是对集合中已存在的文档进行查询,即对应关系型数据库当中的select操作,比如MySQL,MongoDB提供以下几种主要查询文档方法: db.collection.find():查询指定集合中满足条件的一个或多个文档和视图;db.collection.findOne():查询指定集合中满足条件的第一个文档,并以格式化方式展现,通过pretty()方法。来自官方文档的测试数据: db.inventory.insertMany([ { item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" }, { item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "A" }, { item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" }, { item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" }, { item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" }]);回到顶部db.collection.find()db.collection.find()可以说是使用频率最高的方法了,可以用来查询数据库集合当中的文档。 语法格式如下: db.collection.find(, )query:查询表达式;projection:指定查询结果集中需要显示的字段。Col_name:1|true 代表显示该字段;Col_name:0 | false 代表不显示该字段。_id字段是默认显示的,如果不想显示,则显式指定{"_id" : 0}。 查询所有文档: db.inventory.find()或 db.inventory.find({})回到顶部db.collection.findOne()db.collection.findOne()方法显示符合条件查询的第一条文档,接受的参数与db.collection.find()方法一致。 回到顶部条件查询操作符通常对文档的查询,是需要带条件的,而很少使用到不带条件的全文档检索,以下总结了几种常使用的查询操作符: 比较操作符比较操作符涉及的操作如下表所示: 名称 说明$eq 与指定值相等$gt 大于指定的值$gte 大于或等于指定的值$in 指定的值在数组中$lt 小于指定的值$lte 小于或等于指定的值$ne 所有不等于指定的值$nin 指定的值不在数组中使用示例: // $eq:等值查询 SQL: SELECT * FROM inventory WHERE status = "D";db.inventory.find( { status: "D" } ) // $ne 同$eq // $gt:范围查询(以大于为例) SQL: SELECT * FROM inventory WHERE qty &gt; 30;db.inventory.find( { qty: { $gt: 30 } } ) // $gte、$lt、$lte 同$gt // $in:或查询,可使用or代替 SQL: SELECT * FROM inventory WHERE status in ("A", "D")db.inventory.find( { status: { $in: [ "A", "D" ] } } ) // $nin 同$in逻辑操作符逻辑操作符涉及的操作如下表所示: 名称 说明$and 指定查询同时满足多个条件查询子句$not 指定查询不满足条件查询子句$nor 指定查询无法满足多个条件查询子句$or 指定查询满足其中某个条件查询子句使用示例: // $and: 逻辑与查询 SQL: SELECT * FROM inventory WHERE status = "A" AND qty &lt; 30;db.inventory.find( { $and: [ { status: { $eq: "A" }, qty: { $lt: 30 } } ] } ) // $not: 不符合查询 SQL: SELECT * FROM inventory WHERE status &lt;&gt; "A";db.inventory.find( { status: { $not: { $eq: "A" } } } ) /*$nor: 无法同时满足多个条件查询,字段不存在时也符合 SQL: SELECT * FROM inventory WHERE status &lt;&gt; "A" AND qty &gt; 30; 符合以下条件之一都会出现在结果集中:1.文档包含status和qty字段并且符合条件;2.文档包含status字段并且符合条件,不包含qty字段;3.文档不包含status字段,包含qty字段并且符合条件;4.文档不包含status字段和qty字段。*/db.inventory.find( { $nor: [ { status: { $eq: "A" } }, { qty: { $lt: 30 } } ] } ) // $or: 逻辑或查询 SQL: SELECT * FROM inventory WHERE status = "A" OR qty &lt; 30;db.inventory.find( { $or: [ { status: "A" }, { qty: { $lt: 30 } } ] } )元素操作符元素操作符主要涉及的操作如下表所示: 名称 说明$exists 指定查询文档是否有对应的字段$type 指定查询文档的某个字段是否是对应类型使用示例: // $exists: 是否存在指定字段查询db.inventory.find( { price: { $exists: true } } ) // $type: 字段是否是指定类型查询db.inventory.find( { "qty": { $type: "double" } } )评估操作符评估操作符主要涉及的操作如下表所示,更多操作符可以参考官方文档:https://docs.mongodb.com/manual/reference/operator/query-evaluation/。 名称 说明$expr 为同一个文档中的字段指定表达式并且符合条件的查询,比如比较同一文档当中两个字段的值$mod 为字段值取模并且符合条件的查询为了更好的使用这两个主要的操作符,额外创建个文档: db.monthlyBudget.insertMany([ { "_id" : 1, "category" : "food", "budget": 400, "spent": 450 }, { "_id" : 2, "category" : "drinks", "budget": 100, "spent": 150 }, { "_id" : 3, "category" : "clothes", "budget": 100, "spent": 50 }, { "_id" : 4, "category" : "misc", "budget": 500, "spent": 300 }, { "_id" : 5, "category" : "travel", "budget": 200, "spent": 650 } ]);使用示例: // $expr: 允许使用聚合表达式,这里以$gt为例,更多表达式参考 https://docs.mongodb.com/manual/meta/aggregation-quick-reference/#aggregation-expressionsdb.monthlyBudget.find( { $expr: { $gt: [ "$spent" , "$budget" ] } } ) // $mod: 对字段所在值进行取模运算,显示符合条件的查询,如qty字段值对4取模,并且余数为0db.inventory.find( { qty: { $mod: [ 4, 0 ] } } )回到顶部更新(Update Operations)更新(Update Operations)是对已存在的文档进行修改操作,MongoDB提供以下几种主要更新文档方法: db.collection.update():更新或替换集合中符合条件的一个或多个文档;db.collection.updateOne():只更新集合中符合条件的第一个文档,即使有多个文档(版本3.2新增);db.collection.updateMany():更新集合中所有符合条件的文档(版本3.2新增)。回到顶部db.collection.update()根据update指定的表达式可以修改文档中符合条件的字段或代替整个文档。具体的语法格式如下: db.collection.update(, //查询表达式, //更新表达式 { upsert: &lt;boolean&gt;, multi: &lt;boolean&gt;, writeConcern: &lt;document&gt;, collation: &lt;document&gt;, arrayFilters: [ &lt;filterdocument1&gt;, ... ], hint: &lt;document|string&gt; // 版本4.2新增 })参数说明: query:更新文档的查询表达式;如果指定了参数upsert: true并且集合中没有符合查询条件的文档,查询条件中有关于字段_id指定了.分隔符的,并不会插入新的文档;update:主要包含三种格式1.更新文档:只包含更新操作符表达式;2.替换文档:只包含: 对;3.聚合管道:版本4.2新增,详细参考官方文档。upsert:当query查询条件没符合更新的文档,就新创建文档(可选),默认值为false;multi:是否更新多个符合条件的文档(可选),默认值为false,只更新符合条件的第一个文档;writeConcern:参考db.collection.insert()相同参数说明;collation:指定校对规则(可选,版本3.4新增);arrayFilters:文档数组更新过滤操作符(可选,版本3.6新增);详细参考:https://docs.mongodb.com/manual/reference/method/db.collection.update/#specify-arrayfilters-for-array-update-operations hint:采用文档或字符串的形式指定适用于查询表达式的索引,如果索引不存在则报错(可选,版本4.2新增)。使用示例: 使用示例将通过使用两种场景进行,一是没有使用参数选项upsert,二是使用参数选项upsert。 不使用选项upsert// 测试数据db.books.remove({}); db.books.insertMany([ { "_id" : 1, "item" : "TBD", "stock" : 0, "info" : { "publisher" : "1111", "pages" : 430 }, "tags" : [ "technology", "computer" ], "ratings" : [ { "by" : "ijk", "rating" : 4 }, { "by" : "lmn", "rating" : 5 } ], "reorder" : false }, { "_id" : 2, "item" : "XYZ123", "stock" : 15, "info" : { "publisher" : "5555", "pages" : 150 }, "tags" : [ ], "ratings" : [ { "by" : "xyz", "rating" : 5 } ], "reorder" : false /* 使用选项参数 upsert: true1、如果查询表达式找到匹配的文档,则执行更新操作;2、如果查询表达式没有找到匹配的文档,则执行插入操作;*/db.books.update( { item: "ZZZ135" }, // 查询表达式 { // 更新或替换文档 item: "ZZZ135", stock: 5, tags: [ "database" ] }, { upsert: true }); // 1.使用更新操作表达式/* $set操作符1、查询表达式指定需要更新的文档 _id;2、$inc操作符: stock的字段值+5;3、$set操作符: 替换item字段值,替换嵌入文档info的publisher字段值,替换tags字段值,替换数组ratings的第二个元素值*/db.books.update( { _id: 1 }, { $inc: { stock: 5 }, $set: { item: "ABC123", "info.publisher": "2222", tags: [ "software" ], "ratings.1": { by: "xyz", rating: 3 } });更新之后的文档:{ "_id" : 1, "item" : "ABC123", "stock" : 5, "info" : { "publisher" : "2222", "pages" : 430 }, "tags" : [ "software" ], "ratings" : [ { "by" : "ijk", "rating" : 4 }, { "by" : "xyz", "rating" : 3 } ], "reorder" : false} // 2.为已存在的数组添加元素// $push操作符: 为指定文档数组ratings添加一个元素db.books.update( { _id: 2 }, { $push: { ratings: { "by" : "jkl", "rating" : 2 } } });更新之后的文档:{ "_id" : 2, "item" : "XYZ123", "stock" : 15, "info" : { "publisher" : "5555", "pages" : 150 }, "tags" : [ ], "ratings" : [ { "by" : "xyz", "rating" : 5 }, { "by" : "jkl", "rating" : 2 } ], "reorder" : false } // 3.文档移除字段// $unset操作符: 移除文档的指定字段,为_id:1文档移除tags字段db.books.update( { _id: 1 }, { $unset: { tags: 1 } } );更新后的文档:{ "_id" : 1, "item" : "TBD", "stock" : 0, "info" : { "publisher" : "1111", "pages" : 430 }, "ratings" : [ { "by" : "ijk", "rating" : 4 }, { "by" : "lmn", "rating" : 5 } ], "reorder" : false } // 4.替换整个文档// 替换_id:2的文档db.books.update( { _id: 2 }, { item: "XYZ123", stock: 10, info: { publisher: "2255", pages: 150 }, tags: [ "baking", "cooking" ] });更新后的文档:{ "_id" : 2, "item" : "XYZ123", "stock" : 10, "info" : { "publisher" : "2255", "pages" : 150 }, "tags" : [ "baking", "cooking" ]} // 5.更新多个文档db.books.update( { stock: { $lte: 10 } }, { $set: { reorder: true } }, { multi: true });更新后的全部文档:[ { "_id" : 1, "item" : "ABC123", "stock" : 5, "info" : { "publisher" : "2222", "pages" : 430 "ratings" : [ { "by" : "ijk", "rating" : 4 }, { "by" : "xyz", "rating" : 3 } ], "reorder" : true } { "_id" : 2, "item" : "XYZ123", "stock" : 10, "info" : { "publisher" : "2255", "pages" : 150 }, "tags" : [ "baking", "cooking" ], "reorder" : true }]使用upserts选项/* 使用选项参数 upsert: true1、如果查询表达式找到匹配的文档,则执行更新操作;2、如果查询表达式没有找到匹配的文档,则执行插入操作;*/ // 1.插入未符合更新条件的文档db.books.update( { item: "ZZZ135" }, { item: "ZZZ135", stock: 5, tags: [ "database" ] { upsert: true } );因为集合并未满足条件的文档,则插入的文档为:{ "_id" : ObjectId("5da78973835b2f1c75347a83"), "item" : "ZZZ135", "stock" : 5, "tags" : [ "database" ]} // 2.插入未符合更新条件并且基于更新操作符的文档// 如果没有符合更新查询条件,并且使用的是更新操作符,则会基于当前的查询条件和更新操作符字段插入新的文档db.books.update( { item: "BLP921" }, { $set: { reorder: false }, $setOnInsert: { stock: 10 } }, { upsert: true } );新插入的文档为:{ "_id" : ObjectId("5da79019835b2f1c75348a0a"), "item" : "BLP921", "reorder" : false, "stock" : 10} // 3.插入未符合更新条件并且基于聚合管道的文档// 关于聚合管道请参考官方文档:https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline // 4.插入未符合更新条件并且同时联合多文档操作符的文档如果不符合查询条件,则只会插入单个文档db.books.update( { "info.publisher": "Self-Published" }, { $set: { reorder: false, tags: [ "literature", "hardcover" ], stock: 25 } }, { upsert: true, multi: true } );新插入的文档:{ "_id" : ObjectId("5db337934f670d584b6ca8e0"), "info" : { "publisher" : "Self-Published" }, "reorder" : false, "stock" : 25, "tags" : [ "literature", "hardcover" ]}回到顶部db.collection.updateOne()根据update指定的参数可以修改文档中符合条件的字段或代替整个文档,与db.collection.update()不同的是每次只更新单个文档。 语法格式如下: db.collection.updateOne(,, { upsert: &lt;boolean&gt;, writeConcern: &lt;document&gt;, collation: &lt;document&gt;, arrayFilters: [ &lt;filterdocument1&gt;, ... ], hint: &lt;document|string&gt; })参数说明: 参考db.collection.update()的参数说明。 使用示例: // 参考db.collection.update()回到顶部db.collection.updateMany()根据update指定的参数可以修改文档中符合条件的字段或代替整个文档,与db.collection.updateOne()不同的是更新所有符合条件的文档。 语法格式如下: db.collection.updateMany(,, { upsert: &lt;boolean&gt;, writeConcern: &lt;document&gt;, collation: &lt;document&gt;, arrayFilters: [ &lt;filterdocument1&gt;, ... ], hint: &lt;document|string&gt; })参数说明: 参考db.collection.update()的参数说明。 使用示例: // 参考db.collection.update()回到顶部删除(Delete Operations)删除是指对集合当中已存在的文档进行清除操作,MongoDB提供以下几种主要删除文档方法: db.collection.deleteOne():只删除集合中符合条件的一个文档;db.collection.deleteMany():删除集合中所有符合条件的文档;db.collection.remove():删除集合中符合条件的一个或多个文档。回到顶部db.collection.deleteOne()根据filter选项条件删除集合中的单个文档,具体语法格式如下: db.collection.deleteOne(, { writeConcern: &lt;document&gt;, collation: &lt;document&gt; })参数说明: filter:指定基于查询表达式的过滤条件,关于查询表达式可以查看db.collecion.find()中的;writeConcern:参考db.collection.insert()相同参数说明;collation:指定校对规则(可选,版本3.4新增);使用示例: // 删除指定条件的单个文档db.orders.deleteOne( { "_id" : 1 } );{ "acknowledged" : true, "deletedCount" : 1 }回到顶部db.collection.deleteMany()根据filter选项条件删除集合中的单个文档,具体语法格式如下: db.collection.deleteMany(, { writeConcern: &lt;document&gt;, collation: &lt;document&gt; })参数说明: 参考db.collection.deleteOne()的参数说明。 使用示例: // 删除指定条件的多个文档db.orders.deleteMany( {"cust_id" : "Cam Elot"} );{ "acknowledged" : true, "deletedCount" : 2 }注意: 如果是对固定集合进行删除文档操作则会报错,固定集合的清除操作使用方法db.collection.drop()。 回到顶部总结本文简单梳理了在Mongo Shell下基本的CRUD操作,主要适用于DBA的运维管理,如果是研发同学,根据不同的编程语言使用不同客户端驱动进行操作,详细同样可以参考官方文档;针对CRUD各个方面还有其他一些额外的方法,比如查询修改文档方法db.collection.findAndModify(),这里只是总结每个文档操作中一些最基础的方法,对于额外高级的方法这里不再赘述;掌握了这些基本的CRUD操作,就可以对MongoDB文档进行操作了,但还是需要控制好权限,毕竟数据安全不是小事,做变更之前做好数据的备份,以防万一。回到顶部参考https://docs.mongodb.com/manual/crud/ https://docs.mongodb.com/manual/reference/operator/query-evaluation/ https://docs.mongodb.com/manual/reference/method/db.collection.update/#specify-arrayfilters-for-array-update-operations https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline ☆〖本人水平有限,文中如有错误还请留言批评指正!〗☆ 作者: H_Johnny出处: http://www.cnblogs.com/dbabd/

Java 异常(一) 异常概述及其架构

Java 异常(一) 异常概述及其架构 一、异常概述(一)、概述Java异常是Java提供的一种识别及响应错误的一致性机制。异常指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答 what, where, why 这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪“抛出,异常信息回答了“为什么“会抛出。 (二)、异常体系异常机制其实是帮助我们找到程序中的问题,异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Error与java.lang.Exception,平常所说的异常指java.lang.Exception。 Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身可以处理,错误是无法处理。 1、Throwable:Throwable是 Java 语言中所有错误或异常的超类。Throwable包含两个子类: Error 和 Exception。它们通常用于指示发生了异常情况。 Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。 Throwable常用API: public void printStackTrace() // 打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置。public String getMessage() // 获取发生异常的原因。public String toString() // 获取异常的类型和异常描述信息。2、Error:严重错误Error,无法通过处理的错误,只能事先避免。 3、Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。 (三)、异常分类我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。 异常主要分为两大类:编译期异常和运行期异常。 编译期异常:checked异常。在编译期,就会检查,如果没有处理异常,则编译失败(如日期格式化异常)。 特点: Java编译器会检查它。此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。被检查异常通常都是可以恢复的。 运行期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)(如数学异常)。 特点: Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。 虽然Java编译器不会检查运行时异常,但是我们也可以通过throws进行声明抛出,也可以通过try-catch对它进行捕获处理。 如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生! 二、异常处理Java异常处理机制用到的几个关键字:try、catch、finally、throw、throws。 try:用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。catch:用于捕获异常。catch用来捕获try语句块中发生的异常。finally:finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。throw:用于抛出异常。throws:用在方法签名中,用于声明该方法可能抛出的异常。三、异常注意点(一)、finally中的异常会覆盖(消灭)前面try或者catch中的异常不要在fianlly中使用return。不要在finally中抛出异常。减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。将尽量将所有的return写在函数的最后面,而不是try ... catch ... finally中。(二)、多个异常使用捕获多个异常分别处理。多个异常一次捕获,多次处理。多个异常一次捕获一次处理。一般情况下,一般我们是使用一次捕获多次处理方式,如下代码 // 编写可能会出现异常的代码 }catch(异常类型A e){ 当try中出现A类型异常,就用该catch来捕获. // 处理异常的代码 //记录日志/打印异常信息/继续抛出异常 }catch(异常类型B e){ 当try中出现B类型异常,就用该catch来捕获. // 处理异常的代码 //记录日志/打印异常信息/继续抛出异常 注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。 三、实例(一)、try - catch用法try - catch 必须搭配使用,不能单独使用。 public class ExceptionDemo { public static void main(String[] args) { try { int i = 10 / 0;// 除数不能为0,此行会抛出 ArithmeticException 异常 System.out.println("i = " + i);// 抛出异常后,此行不会执行 }catch(ArithmeticException e) { // 捕获 ArithmeticException 异常 // 在catch 代码块处理异常 e.printStackTrace(); // 异常最详细信息 System.out.println("e.getMessage() : " + e.getMessage());// 发生异常的原因 System.out.println("e.toString() : " + e.toString()); // 获取异常的类型和异常描述信息 (二)、finally 用法try - catch - finally搭配使用,或者 try - finally 搭配使用。 public class ExceptionDemo { public static void main(String[] args) { // try-catch-finally搭配使用 try { int[] arr = {1,2,3}; int i = arr[3];// 数组索引越界,此行会抛出 ArrayIndexOutOfBoundsException 异常 System.out.println("i = " + i);// 抛出异常后,此行不会执行 }catch(ArithmeticException e) { // 捕获 ArithmeticException System.out.println(e.getMessage());// 发生异常的原因 System.exit(0); // 程序强制退出,finally 代码块不会执行 }finally {// 除了程序强制退出,如(System。exit(0)),无论是否发生异常,finally 代码块总会执行 System.out.println("this is finally"); // try-finally搭配使用 try { int[] arr = {1,2,3}; int i = arr[3];// 数组索引越界,此行会抛出 ArrayIndexOutOfBoundsException 异常 System.out.println("i = " + i);// 抛出异常后,此行不会执行 }finally { // 无论是否发生异常,finally 代码块总会执行 System.out.println("this is finally"); try-catch-finally 搭配:这种形式捕获异常时,开发者可以在 catch 代码块中处理异常(如打印日志、日志记录等),异常处理权在开发者。try-finally 搭配:这种形式捕获异常时,默认抛出给 JVM 处理,JVM默认处理时调用 e.printStackTrace() 方法打印异常详细信息。finally 代码块:除非程序强制退出,否则无论程序是否发生异常,finally 代码块总会执行。finally 中抛出异常会覆盖(消灭)前面 try 或者 catch 中的异常,尽量避免在 finally 代码块中抛出异常。如果 try 中和 finally 中都有 return 语句,程序会先执行 finally 中的 return 语句,然后程序块运行结束,而不会执行 try 中的 return 语句。所以尽量不在finally中使用 return 语句。(三)、throw 用法throw 是用于抛出异常,将这个异常对象传递到调用者处,并结束当前方法的执行 public static void main(String[] args) {  try {    int i = 10 / 0;    System.out.println("i = " + i);  }catch(ArithmeticException e) {     // 抛出异常,传递自定义异常信息提示    // 默认抛出给 JVM 处理打印异常详细信息     throw new ArithmeticException("除数不能为0");  } } (四)、throws 用法throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)。 public class ExceptionDemo { public static void main(String[] args) { demo(); public static void demo() throws ArrayIndexOutOfBoundsException{ try { int[] arr = {1,2,3}; int i = arr[3]; System.out.println("i = " + i); }catch(ArrayIndexOutOfBoundsException e) { System.out.println(e.toString()); 原文地址https://www.cnblogs.com/lingq/p/12942895.html

(二)C#网络编程入门之TCP (三)C#网络编程入门之HTTP 一、概述UDP和TCP是网络通讯常用的两个传输协议,C#一般可以通过Socket来实现UDP和TCP通讯,由于.NET框架通过UdpClient、TcpListener 、TcpClient这几个类对Socket进行了封装,使其使用更加方便, 本文就通过这几个封装过的类讲解一下相关应用。 二、基本应用:连接、发送、接收服务端建立侦听并等待连接: TcpListener tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), 9000);tcpListener.Start();if (tcpListener.Pending()){ TcpClient client = tcpListener.AcceptTcpClient(); Console.WriteLine("Connected"); 服务端是通过AcceptTcpClient方法获得TcpClient对象,而客户端是直接创建TcpClient对象。 TcpClient tcpClient = new TcpClient();tcpClient.Connect("127.0.0.1", 9000);发送数据TcpClient对象创建后,发送接收都通过TcpClient对象完成。 发送数据: TcpClient tcpClient = new TcpClient(); tcpClient.Connect("127.0.0.1", 9000); NetworkStream netStream = tcpClient.GetStream(); int Len = 1024; byte[] datas = new byte[Len]; netStream.Write(datas, 0, Len); netStream.Close(); tcpClient.Close(); 接收数据: TcpClient client = tcpListener.AcceptTcpClient();Console.WriteLine("Connected"); NetworkStream stream = client.GetStream(); var remote = client.Client.RemoteEndPoint; byte[] data = new byte[1024]; while (true) { if (stream.DataAvailable) int len = stream.Read(data, 0, 1024); Console.WriteLine($"From:{remote}:Received ({len})"); Thread.Sleep(1); 三、 粘包问题和UDP不太一样,TCP连接不会丢包,但存在粘包问题。(严格来说粘包这个说法是不严谨的,因为TCP通讯是基于流的,没有包的概念,包只是使用者自己的理解。) 下面分析一下粘包产生的原因及解决办法。 TCP数据通讯是基于流来实现的,类似一个队列,当有数据发送过来时,操作系统就会把发送过来的数据依次放到这个队列中,对发送者而言,数据是一片一片发送的,所以自然会认为存在数据包的概念,但对于接收者而言,如果没有及时去取这些数据,这些数据依次存放在队列中,彼此之间并无明显间隔,自然就粘包了。 还有一种情况粘包是发送端造成的,有时我们调用发送代码时,操作系统可能并不会立即发送,而是放到缓存区,当缓存区达到一定数量时才真正发送。 要解决粘包问题,大致有以下几个方案。 1、 约定数据长度,发送端的数据都是指定长度,比如1024;接收端取数据时也取同样长度,不够长度就等待,保证取到的数据和发送端一致; 2、 接收端取数据的频率远大于发送端,比如发送端每1秒发送一段数据,接收端每0.1秒去取一次数据,这样基本可以保证数据不会粘起来; 以上两个方案都要求发送端需要立即发送,不可缓存数据。而且这两种方案都有缺陷:首先,第一种方案:如果要包大小一致的话,如果约定的包比较大,肯定有较多数据冗余,浪费网络资源,如果包较小,连接就比较频繁,效率不高。 其次,第二种方案:这个方案只能在理想环境下可以实现,当服务端遭遇一段时间的计算压力时可能会出现意外,不能完全保证。 比较完善的解决方案就是对接收到的数据进行预处理:首先通过定义特殊的字符组合作为包头和包尾,如果传输ASCII字符,可以用0x02表示开始(STX),用0x03表示结束(ETX),比如:STX ‘H’ ‘e’ ‘l’ ‘l’ ‘o’ ETX (二进制数据: 02 48 65 6C 6C 6F 03)。如果数据较长可以在包头留出固定位置存放包长度, 如: 02 00 05 48 65 6C 6C 6F 03 其中02 05 就表示正文长度为5个字节,可以进行校验。 虽然第三种方案比较严谨,但相对复杂,在传输比较可靠、应用比较简单的场景下,也可以采用前面两种解决方案。 四、 一个完整的例程服务端:  View Code客户端:  View Code原文地址https://www.cnblogs.com/seabluescn/p/12972632.html

Java 集合排序策略接口 Comparator

Java 集合排序策略接口 Comparator 前言最近用到了集合排序(基于 Java 8)。现在我能用 Stream 的就用 Stream ,真香!排序可以这么写: List peoples = new ArrayList&lt;&gt;(); // 中间省略 // 按照年龄从小到大排序peoples.sort(Comparator.comparing(People::getAge));这里排序用到了一个关键接口 java.util.Comparator。排序比较作为业务中经常出现的需求,我们有必要研究一下这个接口。 Comparator 概念Comparator 是一个函数式接口。它经常用于没有天然排序的集合进行排序,如 Collections.sort 或 Arrays.sort。或者对于某些有序数据结构的排序规则进行声明,如 TreeSet 、TreeMap 。也就是该接口主要用来进行集合排序。 Comparator 中的方法Comparator 作为一个函数式接口只有一个抽象方法,但是它有很多的默认方法,我们来认识一下这些方法们。 3.1 compare 抽象方法作为Comparator 唯一的抽象方法,int compare(T o1,T o2) 比较两个参数的大小, 返回负整数,零,正整数 ,分别代表 o1o2,通常分别返回 -1、0 或 1。伪表达式: // 输入两个同类型的对象 ,输出一个比较结果的int数字(x1,x2)-&gt; int实现该方法一定要注意以下事项: 必须保证compare(x,y) 和compare(y,x) 的值的和必须为 0 。必须保证比较的顺序关系是可传递的,如果compare(x,y)&gt;0 而且compare(y,z)&gt;0 则 compare(x,z)&gt;0。如果存在 compare(x,y)=0,则对于 z 而言,存在 compare(x, z)==compare(y, z)。然而并不 严格要求(compare(x, y)==0) == (x.equals(y))。一般说来,任何违背这个条件的 Comparator 实现都应该明确指出这一事实情况。 3.2 comparing 系列方法从 Java 8 开始,Comparator 提供了一系列的静态方法,并通过函数式的风格赋予 Comparator 更加强大和方便的功能,我们暂且称它们为 comparing系列方法。 public static Comparator comparing( Function&lt;? super T, ? extends U&gt; keyExtractor, Comparator&lt;? super U&gt; keyComparator) Objects.requireNonNull(keyExtractor); Objects.requireNonNull(keyComparator); return (Comparator&lt;T&gt; &amp; Serializable) (c1, c2) -&gt; keyComparator.compare(keyExtractor.apply(c1), keyExtractor.apply(c2)); 该方法是该系列方法的基本方法。是不是看上去很难懂的样子?我们来分析一下该方法。它一共两个参数都是函数式接口。 第一个参数 Function&lt;? super T, ? extends U&gt; keyExtractor 表示输入一个是 T 类型对象,输出一个 U 类型的对象,举个例子,输入一个 People 对象返回其年龄 Integer 数值: // people -&gt; people.getAge(); 转换为下面方法引用Function getAge = People::getAge;第二个参数 keyComparator就很好理解了,表示使用的比较规则。 对 c1,c2 按照 第一个参数 keyExtractor 提供的规则进行提取特征,然后第二个参数keyComparator对这两个特征进行比较。下面的式子其实可以概括为 3.1 的 (x1,x2)-&gt; int (c1, c2) -&gt; keyComparator.compare(keyExtractor.apply(c1), keyExtractor.apply(c2)) Comparator &amp; Serializable 为 Java 8 新特性:同时满足这两个类型约束 理解了这个方法后,其它该系列的方法就好理解了,这里不再赘述。目前 comparing 系列方法使用更加广泛。我们举一些例子: List peoples = new ArrayList&lt;&gt;();// ………………// 按照年龄从低到高排序peoples.sort(Comparator.comparing(People::getAge));// 按照年龄从高到低排序peoples.sort(Comparator.comparing(People::getAge, (x, y) -&gt; -x.compareTo(y))); 同样你可以使用 java.util.Collections 或者 Stream 提供的排序方法来使用Comparator。 小结今天对 Comparator进行了简单的分析,它用于构建集合排序的规则,在日常开发中非常有用。下一篇 我们将对另一个和它十分相似的接口 Comparable 进行分析和比较它们的不同,敬请关注。 原文地址https://www.cnblogs.com/felordcn/p/12921857.html

你了解C#的协变和逆变吗

你了解C#的协变和逆变吗 从C# 4.0开始,泛型接口和泛型委托都支持协变和逆变,由于历史原因,数组也支持协变。里氏替换原则:任何基类可以出现的地方,子类一定可以出现。协变(out)协变:即自然的变化,遵循里氏替换原则,表现在代码上则是任何基类都可以被其子类赋值,如Animal = Dog、Animal = Cat使用out关键字声明(注意和方法中修饰参数的out含义不同)被标记的参数类型只能作为方法的返回值(包括只读属性)在没有协变时:abstract class Animal {}class Dog : Animal {}class Cat : Animal {} interface IPoppable{ T Pop(); }class MyStack : IPoppable{ private int _pos; private readonly T[] _data = new T[100]; public void Push(T obj) =&gt; _data[_pos++] = obj; public T Pop() =&gt; _data[--_pos]; }以下代码是无法通过编译的 var dogs = new MyStack();IPoppable animals1 = dogs; // 此处会发生编译错误Stack animals2 = dogs; // 此处会发生编译错误此时,我们如果需要为动物园饲养员新增一个输入参数为Stack饲喂的方法,一个比较好的方法是新增一个约束泛型方法: class Zookeeper{ public static void Feed&lt;T&gt;(IPoppable&lt;T&gt; animals) where T : Animal {} }// 或者class Zookeeper{ public static void Feed&lt;T&gt;(Stack&lt;T&gt; animals) where T : Animal {} // MainZookeeper.Feed(dogs);现在,C#增加了协变使IPoppable接口支持协变// 仅仅增加了一个 out 声明interface IPoppable{ T Pop(); }简化Feed方法 class Zookeeper{ public static void Feed(IPoppable&lt;Animal&gt; animals) {} // MainZookeeper.Feed(dogs);协变的天然特性——仅可作为方法返回值,接口(或委托)外部无法进行元素添加,确保了泛型类型安全性,所以不用担心Dog的集合中出现Cat 常用的支持协变的接口和委托有:IEnumerableIEnumeratorIQueryableIGroupingFunc等共17个ConverterIEnumerable dogs = Enumerable.Empty();IEnumerable animals = dogs; var dogList = new List();IEnumerable animals = dogList;另外,由于历史原因,数组也支持协变,例如var dogs = new Dog[10];Animal[] animals = dogs;但是无法保证类型安全性,以下代码可正常进行编译,但是运行时会报错 animals[0] = new Cat(); // 运行时会报错逆变(in)逆变:即协变的逆向变化,实质上还是遵循里氏替换的原则,将子类赋值到基类上使用in关键字声明被标记的参数类型只能作为方法输入参数(包括只写属性)例如:abstract class Animal {}class Dog : Animal {}class Cat : Animal {} interface IPushable{ void Push(T obj); }class MyStack : IPushable{ private int _pos; private readonly T[] _data = new T[100]; public void Push(T obj) =&gt; _data[_pos++] = obj; public T Pop() =&gt; _data[--_pos]; // Mainvar animals = new MyStack();animals.Push(new Cat());IPushable dogs = animals;dogs.Push(new Dog());逆变的天然特性——仅可作为方法输入参数,接口(或委托)无法进行元素获取,即只能将子类赋值到父类上,进而保证了类型安全性。 另外,常用支持逆变的接口和委托有:IComparerIComparableIEqualityComparerAction等共16个PredicateComparisonConverterAction animalAction = new Action(a =&gt; { });Action DogAction = animalAction;作者:xiaoxiaotank出处:https://www.cnblogs.com/xiaoxiaotank/

三分钟快速搭建分布式高可用的Redis集群

三分钟快速搭建分布式高可用的Redis集群 这里的Redis集群指的是Redis Cluster,它是Redis在3.0版本正式推出的专用集群方案,有效地解决了Redis分布式方面的需求。当单机内存、并发、流量等遇到瓶颈的时候,可以采用这种Redis Cluster方案进行解决。 分区规则Redis Cluster采用虚拟槽(slot)进行数据分区,即使用分散度良好的哈希函数把所有键映射到一个固定范围的整数集合里,这里的整数就是槽(slot)。Redis Cluster槽的范围是0~16383,计算公式:slot=CRC16(key) &amp; 16383。 白嫖小贴士:CRC16是一种高质量的哈希算法,可以使每个槽所映射的键通常比较均匀。 当集群中有3个节点时,每个节点平均大概负责5461个槽以及槽所映射的键值数据。这样一来,可以解耦数据与节点之间的关系,简化节点扩容和缩容的难度。节点自身维护槽的映射关系,不需要客户端或代理服务维护分区信息。 不过,Redis Cluster相对于单机还是存在一些限制的,比如: 批量操作键支持有限,仅支持具有相同槽的键进行批量操作。事务操作键支持有限,仅支持在同一个节点上多个键的事务操作。不支持多个数据空间。单机Redis可以支持16个数据库,而Cluster模式下只能使用一个数据库空间。扯了这么多Redis Cluster的分区规则,下面我们开始步入正题。 手动搭建把Redis Cluster搭建起来总共几步?答:三步!第一步把冰箱门打开。第二步把大象关进去。第三步把冰箱门带上。不好意思,段子暴露年龄了。集群搭建需要以下三个步骤: 准备节点。节点握手。分配槽。Redis Cluster由多个节点组成,节点数量至少有6个才能组成一个完整高可用的集群,其中有3个主节点和3个从节点,我们就以此为例搭建一个Redis Cluster。 准备节点首先,为6个节点(同一台机器上的6380、6381、6382、6383、6384、6385端口)分别创建配置文件,以6380端口的节点为例: port 6380 logfile "log/redis-6380.log" 开启集群模式 cluster-enabled yes 集群配置文件 cluster-config-file "data/nodes-6380.conf"保持文件名为redis-6380.conf,其他节点的配置文件替换成各自的端口。准备好配置文件后启动所有节点,命令如下: src/redis-server conf/redis-6380.conf &amp;src/redis-server conf/redis-6381.conf &amp;src/redis-server conf/redis-6382.conf &amp;src/redis-server conf/redis-6383.conf &amp;src/redis-server conf/redis-6384.conf &amp;src/redis-server conf/redis-6385.conf &amp;检测日志是否正确,以下是6380端口的节点的日志: # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo # Redis version=4.0.14, bits=64, commit=00000000, modified=0, pid=3031, just started # Configuration loaded No cluster configuration found, I'm df1ac987f47dea35f1d0a83c3b405f0ef86892ab Running mode=cluster, port=6380.6380端口的节点启动成功,第一次启动时如果没有集群配置文件,Redis会自动创建一个。6380端口的节点创建的集群配置文件如下: df1ac987f47dea35f1d0a83c3b405f0ef86892ab :0@0 myself,master - 0 0 0 connectedvars currentEpoch 0 lastVoteEpoch 0集群文件中记录的集群的状态,这里最重要的是节点ID,它是一个40位的16进制字符串,用于唯一标识集群中的这个节点。同样,也可以通过cluster nodes命令查看集群节点状态。比如在6380端口的节点上执行命令: 127.0.0.1:6380&gt; cluster nodesdf1ac987f47dea35f1d0a83c3b405f0ef86892ab :6380@16380 myself,master - 0 0 0 connected目前,我们已经成功启动了6个节点,但是它们只能识别自己的节点信息,互相之间并不认识。下面我们通过节点握手让这6个节点互相之间建立联系从而组成一个集群。 节点握手节点握手是一些运行在集群模式下的节点通过Gossip协议互相通信,达到感知彼此的过程。 白嫖小贴士:Gossip协议是基于流行病传播方式的节点或者进程之间信息交换的协议,在分布式系统中被广泛使用。 节点握手通过客户端执行cluster meet命令实现,它是一个异步命令,执行之后立刻返回,在Redis内部异步发起与目标节点的握手通信,该命令的语法如下: cluster meet 目标节点IP 目标节点端口把6个节点加到一个集群中: 127.0.0.1:6380&gt; cluster meet 127.0.0.1 6381OK127.0.0.1:6380&gt; cluster meet 127.0.0.1 6382OK127.0.0.1:6380&gt; cluster meet 127.0.0.1 6383OK127.0.0.1:6380&gt; cluster meet 127.0.0.1 6384OK127.0.0.1:6380&gt; cluster meet 127.0.0.1 6385OK只需要在集群中任意节点上执行cluster meet命令加入新的节点,握手状态会通过消息在集群中传播,其他节点也会自动发现新节点并与之发起握手流程。 我们再执行一下cluster nodes命令,检查一下6个节点是否已经组成集群: 127.0.0.1:6380&gt; cluster nodes1e1f45677d7b9b0130d03193f0bcec34578ac47d 127.0.0.1:6385@16385 master - 0 1586617919021 5 connecteddf1ac987f47dea35f1d0a83c3b405f0ef86892ab 127.0.0.1:6380@16380 myself,master - 0 1586617916000 2 connected5846b66ebe4fb4a5dcfd035652cc471f7e412752 127.0.0.1:6381@16381 master - 0 1586617917005 1 connecteda435cf98c3444b0b110a224401e397a107c453ef 127.0.0.1:6384@16384 master - 0 1586617914988 4 connected71e0e9e9a6f0c7c85dbe0d396846a9072625c5e8 127.0.0.1:6383@16383 master - 0 1586617918013 3 connectede25590603c7a254cce43aa8437861c5c425d753d 127.0.0.1:6382@16382 master - 0 1586617916000 0 connected可以看到,6个节点都在集群中了。不过,此时因为还没有为集群中的节点分配槽,集群还处于下线状态,所有的数据读写都是被禁止的。比如: 127.0.0.1:6380&gt; set onemore study(error) CLUSTERDOWN Hash slot not served接下来,我们为集群中的节点分配槽。 分配槽我们把6380、6382、6384端口的节点作为主节点,负责处理槽和相关数据;6381、6383、6385端口的节点分别作为从节点,负责故障转移。先把16384个槽平均分配给6380、6382、6384端口的节点,为节点分配槽是通过cluster addslots命令实现: ./redis-cli -h 127.0.0.1 -p 6380 cluster addslots {0..5461} ./redis-cli -h 127.0.0.1 -p 6382 cluster addslots {5462..10922} ./redis-cli -h 127.0.0.1 -p 6384 cluster addslots {10923..16383} OK我们再执行一下cluster nodes命令,检查一下槽是否已经分配: 127.0.0.1:6380&gt; cluster nodes1e1f45677d7b9b0130d03193f0bcec34578ac47d 127.0.0.1:6385@16385 master - 0 1586619468000 5 connecteddf1ac987f47dea35f1d0a83c3b405f0ef86892ab 127.0.0.1:6380@16380 myself,master - 0 1586619464000 2 connected 0-54615846b66ebe4fb4a5dcfd035652cc471f7e412752 127.0.0.1:6381@16381 master - 0 1586619467000 1 connecteda435cf98c3444b0b110a224401e397a107c453ef 127.0.0.1:6384@16384 master - 0 1586619467000 4 connected 10923-1638371e0e9e9a6f0c7c85dbe0d396846a9072625c5e8 127.0.0.1:6383@16383 master - 0 1586619467348 3 connectede25590603c7a254cce43aa8437861c5c425d753d 127.0.0.1:6382@16382 master - 0 1586619468355 0 connected 5462-10922再使用cluster replicate命令把一个节点变成从节点.,这个命令必须在从节点上运行,它的语法是: cluster replicate 主节点ID把6381、6383、6385端口的节点变成对应6380、6382、6384端口的节点的从节点: ./redis-cli -h 127.0.0.1 -p 6381 127.0.0.1:6381&gt; cluster replicate df1ac987f47dea35f1d0a83c3b405f0ef86892abOK127.0.0.1:6381&gt; exit ./redis-cli -h 127.0.0.1 -p 6383 127.0.0.1:6383&gt; cluster replicate e25590603c7a254cce43aa8437861c5c425d753dOK127.0.0.1:6383&gt; exit ./redis-cli -h 127.0.0.1 -p 6385 127.0.0.1:6385&gt; cluster replicate a435cf98c3444b0b110a224401e397a107c453efOK127.0.0.1:6385&gt; exit我们再执行一下cluster nodes命令,检查一下集群状态和主从关系: 127.0.0.1:6380&gt; cluster nodesdf1ac987f47dea35f1d0a83c3b405f0ef86892ab 127.0.0.1:6380@16380 myself,master - 0 1586620148000 2 connected 0-54615846b66ebe4fb4a5dcfd035652cc471f7e412752 127.0.0.1:6381@16381 slave df1ac987f47dea35f1d0a83c3b405f0ef86892ab 0 1586620150000 2 connectede25590603c7a254cce43aa8437861c5c425d753d 127.0.0.1:6382@16382 master - 0 1586620151000 0 connected 5462-1092271e0e9e9a6f0c7c85dbe0d396846a9072625c5e8 127.0.0.1:6383@16383 slave e25590603c7a254cce43aa8437861c5c425d753d 0 1586620152220 3 connecteda435cf98c3444b0b110a224401e397a107c453ef 127.0.0.1:6384@16384 master - 0 1586620150000 4 connected 10923-163831e1f45677d7b9b0130d03193f0bcec34578ac47d 127.0.0.1:6385@16385 slave a435cf98c3444b0b110a224401e397a107c453ef 0 1586620149000 5 connected自此,RedisCluster已经手动搭建完成。手动搭建可以理解集群建立的流程和细节,不过大家也会发现手动搭建有很多步骤,当集群的节点比较多的时候,肯定会让人头大。所以Redis官方提供了redis-trib.rb工具,可以让我们快速地搭建集群。 自动搭建redis-trib.rb是使用Ruby开发的Redis Cluster的管理工具,不需要额外下载,默认位于源码包的src目录下,但因为该工具是用Ruby开发的,所以需要准备相关的依赖环境。 环境准备安装Ruby: yum -y install zlib-develwget https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.1.tar.gztar xvf ruby-2.5.1.tar.gzcd ruby-2.5.1/./configure -prefix=/usr/local/rubymakemake installcd /usr/local/ruby/cp bin/ruby /usr/local/bincp bin/gem /usr/local/bin安装rubygem redis依赖: wget http://rubygems.org/downloads/redis-3.3.0.gemgem install -l redis-3.3.0.gem安装redis-trib.rb: cp src/redis-trib.rb /usr/local/bin执行redis-trib.rb命令确认一下环境是否准备正确: redis-trib.rb help Usage: redis-trib create host1:port1 ... hostN:portN --replicas &lt;arg&gt; check host:port info host:port fix host:port --timeout &lt;arg&gt; reshard host:port --from &lt;arg&gt; ...此处省略一万个字...搭建集群像前面的内容讲的,准备好节点配置并启动: src/redis-server conf/redis-7380.conf &amp;src/redis-server conf/redis-7381.conf &amp;src/redis-server conf/redis-7382.conf &amp;src/redis-server conf/redis-7383.conf &amp;src/redis-server conf/redis-7384.conf &amp;src/redis-server conf/redis-7385.conf &amp;使用redis-trib.rb create命令完成节点握手和槽分配的工作,命令如下: redis-trib.rb create --replicas 1 127.0.0.1:7380 127.0.0.1:7382 127.0.0.1:7384 127.0.0.1:7381 127.0.0.1:7383 127.0.0.1:7385其中--replicas参数用来指定集群中每个主节点有几个从节点,这里设置的是1。命令执行后,会首先给出主从节点的分配计划: Creating clusterPerforming hash slots allocation on 6 nodes...Using 3 masters: 127.0.0.1:7380127.0.0.1:7382127.0.0.1:7384Adding replica 127.0.0.1:7383 to 127.0.0.1:7380Adding replica 127.0.0.1:7385 to 127.0.0.1:7382Adding replica 127.0.0.1:7381 to 127.0.0.1:7384 Trying to optimize slaves allocation for anti-affinity [WARNING] Some slaves are in the same host as their masterM: c25675d021c377c91f860986025e3779d89ede79 127.0.0.1:7380 slots:0-5460 (5461 slots) masterM: 58980a81b49de31383802d7d21d6782881678922 127.0.0.1:7382 slots:5461-10922 (5462 slots) masterM: 3f00a37d2c7a5ea40671c8f2934f66d059157a4a 127.0.0.1:7384 slots:10923-16383 (5461 slots) masterS: 6f7dd93973a8332305831e6b7b5e2c54c15b3b51 127.0.0.1:7381 replicates 3f00a37d2c7a5ea40671c8f2934f66d059157a4aS: 03e01f82a935ed7f977af092e6a9cb71057df68a 127.0.0.1:7383 replicates c25675d021c377c91f860986025e3779d89ede79S: 2cf3883e974a709b7070d6c4d7c528d9fa813358 127.0.0.1:7385 replicates 58980a81b49de31383802d7d21d6782881678922Can I set the above configuration? (type 'yes' to accept):如果我们同意这份计划就输入yes,之后就会开始执行节点握手和槽分配,输入如下: Nodes configuration updatedAssign a different config epoch to each nodeSending CLUSTER MEET messages to join the cluster Waiting for the cluster to join.... Performing Cluster Check (using node 127.0.0.1:7380) M: c25675d021c377c91f860986025e3779d89ede79 127.0.0.1:7380 slots:0-5460 (5461 slots) master 1 additional replica(s)M: 58980a81b49de31383802d7d21d6782881678922 127.0.0.1:7382 slots:5461-10922 (5462 slots) master 1 additional replica(s)S: 2cf3883e974a709b7070d6c4d7c528d9fa813358 127.0.0.1:7385 slots: (0 slots) slave replicates 58980a81b49de31383802d7d21d6782881678922S: 03e01f82a935ed7f977af092e6a9cb71057df68a 127.0.0.1:7383 slots: (0 slots) slave replicates c25675d021c377c91f860986025e3779d89ede79S: 6f7dd93973a8332305831e6b7b5e2c54c15b3b51 127.0.0.1:7381 slots: (0 slots) slave replicates 3f00a37d2c7a5ea40671c8f2934f66d059157a4aM: 3f00a37d2c7a5ea40671c8f2934f66d059157a4a 127.0.0.1:7384 slots:10923-16383 (5461 slots) master 1 additional replica(s)[OK] All nodes agree about slots configuration. Check for open slots...Check slots coverage... [OK] All 16384 slots covered.集群创建完成后,还可以使用redis-trib.rb check命令检查集群是否创建成功,具体命令如下: redis-trib.rb check 127.0.0.1:7380 Performing Cluster Check (using node 127.0.0.1:7380) M: c25675d021c377c91f860986025e3779d89ede79 127.0.0.1:7380 slots:0-5460 (5461 slots) master 1 additional replica(s)M: 58980a81b49de31383802d7d21d6782881678922 127.0.0.1:7382 slots:5461-10922 (5462 slots) master 1 additional replica(s)S: 2cf3883e974a709b7070d6c4d7c528d9fa813358 127.0.0.1:7385 slots: (0 slots) slave replicates 58980a81b49de31383802d7d21d6782881678922S: 03e01f82a935ed7f977af092e6a9cb71057df68a 127.0.0.1:7383 slots: (0 slots) slave replicates c25675d021c377c91f860986025e3779d89ede79S: 6f7dd93973a8332305831e6b7b5e2c54c15b3b51 127.0.0.1:7381 slots: (0 slots) slave replicates 3f00a37d2c7a5ea40671c8f2934f66d059157a4aM: 3f00a37d2c7a5ea40671c8f2934f66d059157a4a 127.0.0.1:7384 slots:10923-16383 (5461 slots) master 1 additional replica(s)[OK] All nodes agree about slots configuration. Check for open slots...Check slots coverage... [OK] All 16384 slots covered.可以看到,所有的槽都已分配到节点上,大功告成! 作者:万猫学社出处:http://www.cnblogs.com/heihaozi/

Signal 协议 的 java接口库 概述Signal协议是一种基于双棘轮技术的前向保密协议。可应用于在同步和异步这两种消息传递环境。 接口库开源地址:https://github.com/signalapp/libsignal-protocol-java 预共享密钥Signal协议中应用了预共享密钥(PreKey)概念。预共享密钥包括一个公钥和一个对应的惟一ID。 准备数据时,客户端生成一个已签名的预共享密钥,以及一组未签名的预共享密钥的列表,并将它们全部传输到服务器上存储,以供查询。预共享密钥也可以签名。 会话Signal协议是面向会话的。客户端建立一个“会话”,用于后续的加密/解密操作。一旦建立了一个会话,通信过程中就不必再拆掉它。 会话可以通过以下三种方式建立: 基于预共享密钥包(PreKeyBundles):预向某收件人发送消息的客户端,可以通过从服务器上检索该收件人的预共享密钥包来建立会话。预共享密钥消息(PreKeySignalMessages):客户端可以从收件人接收预共享密钥消息并使用它建立会话。密钥交换消息(KeyExchangeMessages):两个客户端可以交换密钥交换消息以建立会话。状态已建立的会话封装了两个客户端之间的许多状态。这些状态与会话同生命周期,保存在记录中: 身份信息状态:客户端需要维护自己的身份密钥对以及从其他客户端接收的身份密钥的状态;预共享密钥状态:客户端需要维护生成的预共享密钥的状态;已签名预共享密钥状态:客户端需要维护其已签名预密钥的状态;会话状态:客户端需要维护其已建立会话的状态。接口库使用准备数据准备数据时,libsignal客户端需要生成其身份认证密钥、注册id和预共享密钥。以下代码是以bob端为例: // 构建bob的数据内容int bobId = 0x10203040;// bob 的地址SignalProtocolAddress bobAddress = new SignalProtocolAddress("BobName", bobId);// IKP-B,身份认证密钥对IdentityKeyPair bobIdentityKeyPair = KeyHelper.generateIdentityKeyPair();// 注册编号,int bobRegistrationId = KeyHelper.generateRegistrationId(false);// 预共享密钥List bobPreKeys = KeyHelper.generatePreKeys(bobId, 100);// 已签名预共享密钥SPK-B IDint bobSignedPreKeyId = 1;// 已签名预共享密钥SignedPreKeyRecord bobSignedPreKey = KeyHelper.generateSignedPreKey(bobIdentityKeyPair, bobSignedPreKeyId);// 获取到的,bob的某一个预共享的密钥包// 一次性预共享密钥OPK-B IDint bobPreKeyId = 1;// 一次性预共享密钥OPK-B公钥ECPublicKey bobPreKeyPublic = bobPreKeys.get(bobPreKeyId).getKeyPair().getPublicKey();// 已签名预共享密钥SPK-B公钥ECPublicKey bobSignedPreKeyPublic = bobSignedPreKey.getKeyPair().getPublicKey();// 预共享密钥签名Sig(IK-B,Encode(SPK-B))byte[] bobSignedPreKeySignature = Curve.calculateSignature(bobIdentityKeyPair.getPrivateKey(),bobSignedPreKeyPublic.serialize());// 身份认证公钥IK-BIdentityKey bobIdentityKey = bobIdentityKeyPair.getPublicKey();// 组建预共享密钥包PreKeyBundle bobProKeyBundle = new PreKeyBundle(bobRegistrationId, bobRegistrationId, bobPreKeyId,bobPreKeyPublic, bobSignedPreKeyId, bobSignedPreKeyPublic, bobSignedPreKeySignature, bobIdentityKey);//发送预共享密钥包到服务器...建立会话应用接口库的客户端需要实现四个接口:IdentityKeyStore、PreKeyStore、SignedPreKeyStore和SessionStore。它们将用于管理身份认证密钥、预共享密钥、已签名预共享密钥和会话状态的加载和存储。 通过这些构建会话就相当简单: // IKP-A,身份认证密钥对IdentityKeyPair aliceIdentityKeyPair = KeyHelper.generateIdentityKeyPair();// 注册编号,int aliceRegistrationId = KeyHelper.generateRegistrationId(false);// 会话存储SessionStore aliceSessionStore = new InMemorySessionStore();// 预共享存储PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore();// 已签名预共享密钥SignedPreKeyStore aliceSignedPreKeyStore = new InMemorySignedPreKeyStore();// 身份认证密钥对IdentityKeyStore aliceIdentityStore = new InMemoryIdentityKeyStore(aliceIdentityKeyPair, aliceRegistrationId);// 创建会话处理器SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceSessionStore, alicePreKeyStore,aliceSignedPreKeyStore, aliceIdentityStore, bobAddress);// 从服务器获取bob的预共享密钥包,初始化会话aliceSessionBuilder.process(bobProKeyBundle);//创建消息加解密器SessionCipher sessionCipher = new SessionCipher(aliceSessionStore, alicePreKeyStore, aliceSignedPreKeyStore,aliceIdentityStore, bobAddress);// 组建初始消息CiphertextMessage message = sessionCipher.encrypt("Hello world!".getBytes("UTF-8"));//将消息内容发送到bob...bob端收到后可通过PreKeySignalMessage的方式初始化会话。 //SessionBuilder以及各种store的创建,类似alice端bobSessionBuilder.process(sessionRecord ,(PreKeySignalMessage) message);原文地址https://my.oschina.net/piorcn/blog/4270661

应用开发实践之关系型数据库(以MySql为例)小结

应用开发实践之关系型数据库(以MySql为例)小结 本文主要是对目前工作中使用到的DB相关知识点的总结,应用开发了解到以下深度基本足以应对日常需求,再深入下去更偏向于DB本身的理论、调优和运维实践。不在本文重点关注讨论的内容(可能会提到一些): 具体的DQL、DML、DDL、DCL等语法基础性的概念,如主键、索引、存储过程(注:阿里巴巴规范中禁止使用存储过程)等联合查询,我个人不太喜欢在应用中写过于复杂的SQL,性能和后续维护容易出现问题可能会用到的具体DB特性,如oracle的DATA GUARD有一些属于基础知识或语法但是常用的信息,也会列一下,如join的用法。一、基础 ACIDDB的四大特性,这里简单概括下不具体展开。 原子性(Atomicity):事务操作中的多条SQL,要么全部成功要么全部失败,失败后回滚不对原有数据造成任何影响。一致性(Consistency):事务开始前和结束后,数据库的完整性没有被破坏。如触发器、约束、级联回滚隔离性(Isolation):多个事务支持并发读写。具体隔离级别见后文。持久性(Durability):事务结束后,修改是永久的,不丢失。 范式这里展开讲比较复杂,实践中很少用到,一般满足1NF即可。 高一级必满足低一级。 1NF:每个属性都不可再分,即表的列是最原子的2NF:在1NF基础上,消除非主属性对键的部分依赖。这里不解释非主属性和键的含义,可以简单认为是指不存在列A可以通过列B来获取,如“学生姓名-学号”这种y=f(x)的函数关系。3NF:在2NF的基础之上,消除了非主属性对于码的传递函数依赖BCNF:对于关系模式R,如果每一个函数依赖的决定因素都包含键,则R属于BCNF范式有兴趣可以参考:范式通俗理解:1NF、2NF、3NF和BNCF二、事务 事务的隔离级别3.1 读现象 读现象是伴生于不同的隔离级别出现的。读现象的场景都是在多个事务并发执行的前提下可能出现的: 脏读 —— 一个事务读取了另一个未提交事务执行过程中的数据。此时另一个事务可能会由于提交失败而回滚。不可重复读 —— 一个事务执行过程中多次查询同一条数据但返回了不同查询结果。这说明在事务执行过程中,数据被其他事务修改并提交了。幻读 —— 事务1先行查询了某种数据,在修改或插入提交之前,事务2对此类数据进行了插入或删除并提交,导致了事务1对预期结果的数量变化。3.2 隔离级别未提交读(read uncommited):允许另外一个事务可以看到这个事务未提交的数据。提交读(read commited):保证一个事务提交后才能被另外一个事务读取,而不能读取未提交的数据。可重复读(repeatable read):保持读锁和写锁一直到事务提交,但不提供范围锁,因此不能避免幻读。可序列化(serializable):代价最高但最可靠的事务隔离级别,事务被处理为顺序执行。3.3 隔离级别与读现象不同的隔离级别可以防止读现象。 隔离级别 脏读 不可重复读 幻影读未提交读 可能发生 可能发生 可能发生提交读 - 可能发生 可能发生可重复读 - - 可能发生可序列化 - - -注:为什么提交读不能避免不可重复读?假设A事务需要读取两次变量a,第一次读取时a=10,执行过程中a被事务B修改变成了20,那么A第二次读时a与第一次的结果不同。 3.4 查看DB的隔离级别// 查看当前会话select @@tx_isolation;// 查看当前系统select @@global.tx_isolation;MySql 5.7.14-ALISQL版默认是提交读。 事务传播性(Spring)在多个含有事务方法的相互调用时,事务如何在这些方法间传播。 spring支持7种事务传播行为: propagation_requierd:如果当前没有事务,就新建一个事务;否则加入到这个已有事务中,这是最常见的选择。propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作Spring默认是propagation_requierd。为了便于理解,将以上几种传播行为分类: 传播性的类型 当前不在事务中 当前在事务中 备注propagation_requierd 新建一个事务 加入到当前事务 最常见的选择propagation_supports 非事务执行 加入当前事务 propagation_mandatory 抛异常 加入当前事务 propagation_required_new 新建事务 挂起当前事务 propagation_not_supported 非事务执行 挂起当前事务 propagation_never 非事务执行 抛异常 propagation_nested 新建事务 嵌套事务内执行 事务挂起指当前方法不再受所属的事务控制直到该方法结束。比如A方法起了一个事务,调用B方法时B挂起事务,那么B的所有DB操作都不再受A方法的事务控制,直到B执行结束。 事务嵌套嵌套的事务可以独立于当前事务提交或回滚。 三、性能与优化 执行计划确认SQL在实际执行时的执行情况,如是否走上索引、走了哪个索引、扫描行数、执行顺序(如多个select级联查询) 查看方式explain XXX解读MySql: MySQL_执行计划详细说明 索引相关6.1 聚集/非聚集索引 聚集索引:逻辑上和物理上都是连续的,如主键,一般一个表只有一个聚集索引非聚集索引:逻辑上是连续的但物理上不是以Mysql的InnoDB为例:主键是聚集索引。唯一索引、普通索引、前缀索引等都是二级索引(辅助索引)。 结合B+树的知识,对于聚集索引,索引数据和存储数据是在一起的,比如id-age这个记录。对于非聚集索引,只有索引数据,定位具体的记录需要通过索引来找,也即通过索引找到id,再通过id找到id-age这条记录。 6.2 覆盖索引查询条件和结果全部在一个索引中,MySql不需要通过二级索引查到主键后再查一遍数据就可以返回查询数据。覆盖索引可以大大提升查询效率,举例 select a, b from table_x where c = XXX order by d;其中a、b、c、d全部在索引中,那么这就是覆盖索引。 对于做不到覆盖索引的查询,查到主键后还要回到数据表中把数据查询出来,则称为__回表__。 6.3 索引有序性对于联合索引,建立(a, b, c)相当于建立(a), (a,b), (a,b,c)。在这个索引下,遵循”最左前缀原理“,即先按a排序,再按b排序,最后按c排序。如果缺失了前一列,如where b = xxx,则走不上索引。如果某一列不是等值匹配,如where a&gt;10 and b = 1,则只能部分走上索引,b走不上索引。非等值匹配有&lt;、&gt;、!=、IN、LIKE等。 更完整的可以参考mysql组合索引的有序性 6.4 创建了索引但没有走上的原因使用了&lt;、&gt;、!=、IN、LIKE等(非最左的like,也即like 'xxx%'是可以的)使用or连接查询子句预期使用联合索引,但实际上没有按照最左前缀原理排序(见上文7.3节)字符串类型没有使用引号全表扫描比走索引快where子句中包含了函数或表达式为什么你创建的数据库索引没有生效,索引失效的条件! 行锁和表锁select...for update,走上索引(含主键)是行锁,没走上就是表锁。但是如果索引匹配过多,也会变成表锁。 [转载&amp;整理&amp;链接]mysql 通过测试'for update',深入了解行锁、表锁、索引 索引的B+树https://www.cnblogs.com/tiancai/p/9024351.html https://www.jianshu.com/p/9bd572b0a0d4https://www.jianshu.com/p/23524cc57ca4 简单概括一下:B树的中间节点和叶子节点都有不止一个关键字(key)。B树出现的目的是减少磁盘臂移动的开销从而,尽量减少读写的次数。B+树与B树的不同在于,B+树的数据都在叶子节点上,中间件节点没有数据。应用:由于B树最左前缀匹配的特性,如果用左模糊查询(like "%xxx")是走不上索引的。 四、应用开发 分页查询查询第N页(下标从1开始)数据,每页大小PageSize // 先获取符合条件的总数select count(1) from tableA where XXX// 查询该页// 偏移量,可选 offset = (pageSize-1) * N// 行数 rows = pageSizeselect row1, ..., rowN from tableA where XXX limit offset, rows Join10.1 语法 SELECT Table1.Row1, Table1.Row2, Table2.Row1FROM Table1INNER JOIN Table2ON Table1.Row2 = Table2.Row2ORDER BY Table1.Row110.2 种类inner join( = join),都匹配才返回left join,左表全返回不管右表有没有匹配right join,右表全返回不管左表有没有匹配full join,全返回,左表右表无论对方匹配都返回所有行 MyBatis缓存MyBatis缓存分为两级:一级缓存,SqlSession级别;二级缓存,SqlSessionFactory级别。和通常命名习惯相反,二级缓存的作用范围大于一级缓存,原因是,SqlSession是由SqlSessionFactory创建的。 MyBatis默认开启一级缓存,不开启二级缓存。一级缓存生效于同一个SqlSession,当这个session没有做任何update操作且查询完全相同时,会返回一样的数据。此时,在并发环境下,很有可能会发生这种情况:在一台服务器A上连续查询两次,两次属于同一个SqlSession;中间另一个服务器B对表做了更新,A看到的第二次查询结果仍然是旧的。 关于缓存的细节,如如何判断“同一次查询”、缓存有效期、SqlSession原理,可以自行查阅。推荐mybatis中文官网,有很多原理的介绍。在实践中,spring和mybatis整合以后每次查询都会刷新sqlSession,即一级缓存是无效的。MyBatis缓存系列单独提一下,二级缓存的readOnly默认为false,同一条数据在内存中每个对象都是独立的,可修改相互不影响。可参考如何理解Mybatis二级缓存配置中的readOnly? mybatis和hibernate我在工作中绝大多数时间都用mybatis+spring/springboot写持久层,只有一个应用因为使用SpringDataJPA才对hibernate才做了一些了解。 看了一些资料,了解到二者在写法以外,性能的差别主要在于多表查询这个场景,hibernate会比mybatis慢一些,原因是 hibernate为了保证POJO的数据完整性,需要将关联的数据加载,需要额外地查询更多的数据。 MyBatis和Hibernate相比,优势在哪里? - 郑沐兴的回答 - 知乎此外,JPA如果想运行原生sql,可以使用EntityManager。 水平扩展与垂直扩展13.1 水平扩展——分库分表一般思路 按某一字段将一张表分片,如userId。分片方式:第X位到Y位的值字段hash值特殊值特殊处理,如某KA(Key Account关键客户)数据量较大,单独一个分表13.2 水平扩展——历史库按日期定时同步迁移及清理线上数据查询需要根据日期路由到线上库或历史库 13.3 水平扩展——按业务拆表按业务,已处理数据及未处理数据拆分。如已受理未申请单和已完结申请单分开保存。 13.4 垂直扩展提供更多、更强、容量更大的硬件资源。 13.5 FailOver在计算机术语中,故障转移(英语:failover),即当活动的服务或应用意外终止时,快速启用冗余或备用的服务器、系统、硬件或者网络接替它们工作。 故障转移(failover)与交换转移操作基本相同,只是故障转移通常是自动完成的,没有警告提醒手动完成,而交换转移需要手动进行。 ——wiki FailOver是从应用层面做的,不是单纯DB层面。 13.5.1 背景单库架构,一旦库挂掉整个服务不可用;主备架构,切换时有时间延迟;FailOver从分布上来看仍然是主备架构,但是增加了系统自动切换恢复能力。 13.5.2 思想和去IOE是一致的,用大量相对廉价的硬件,拆分服务,减少单点,提升整体的可用性。 13.5.3 交互模式仅举两个最典型的例子,具体场景需要结合硬件能力和应用架构综合分析。 13.5.3.1 记账型特点: 主备准实时同步,Failover库平时不做读写主备库表结构一致,Failover库不一定和主备库的表一致(可能会少一些不需要用到的表)账户型数据保持最终一致性即可方案: 按比列拆表拆库,降低单个库挂掉时影响用户数正常工作时,主备准实时同步,Failover库不读写主库发生异常时,切换到备库读,Failover库记录操作信息。同时,业务操作尽量分流到不依赖相关库到支路上。主库恢复时,不再写入Failover,将Failover库和主库内容做merge,回写主库,主库再同步备库注:可以采取双写、基于读库(上文中所述,利用oracle的data guard、mysql的replication等)、异步消息等保证主备一致。 13.5.3.2 交易流水型特点: 数据保证创建,不保证推进。即交易下单失败,重新下单failover库交易号与主库通过某些位隔离,不重复方案: 和“记账型”类似,Failover库数据推进业务完成即可可以不回写failover期间的数据,依赖中间件读failover库中数据13.6 读写分离为了解决读大于多于写的场景下数据库瓶颈的一种架构模式。同样需要结合具体业务不能生搬硬套。主要是一写多读的架构,在主库挂掉的场景下有可能需要考虑使用paxos算法来决定新的主库。在做读写分离前,可以先考虑缓存是否能解决当前场景的问题。 binlog记录DB操作(不含查询)及其他执行信息的二进制日志。 可以参考下面两篇文章简单了解下。【原创】研发应该懂的binlog知识(上)【原创】研发应该懂的binlog知识(下) 六、其他话题 零碎的话题想起来就补一些。 15.1 列的默认值对于有默认值的非空列,如果在insert语句中指明了这一列且值为null,插入仍然会报错,此时不会取默认值。让该列取默认值的方式是,不让该列出现在insert语句中。 15.2 索引下推MySql5.6做的优化之一,可以在like查询中提高性能。利用查询子句中能确定的查询条件,减少一次查询匹配到的索引,从而减少回表查询的数据。 延伸话题可以自行研究的话题,限于笔者接触范围和篇幅,不展开来写。 索引建立实践,是否越多越好,应该怎么选择索引列hibernate和mybaits的区别,最大区别是mybatis需要手写sql,用一定的工作量更大的灵活性,利于优化和多表联合查询redo log、undo log,与DB本身的分离以下内容可能被滥用,我在实际工作中几乎没有用到,有兴趣可以自行了解。触发器union视图全表扫描时发生的filesort原理附:”点评“ 《阿里巴巴JAVA开发手册》之MySql规范部分开发中遵守一些事先约定好的规范,有助于提升研发效率(无论是个人还是团队内部或团队之间),避免犯一些重复错误,也有助于后续的维护。对于《阿里巴巴JAVA开发手册》中的规范,限于篇幅并没有写明原因,笔者基于自己的开发经验进行一些点评,供参考。本来是想针对《阿里巴巴JAVA开发手册》MySql规范部分这一部分补一下点评的,但是发现前两天新出的泰山版已经补上很多说明,没必要一一点评,直接下载来看就好:https://files.cnblogs.com/files/wuyuegb2312/《Java开发手册(泰山版)》.pdf.zip 可以看出,前面一部分有很多规范都是和Java OOP相关联的。对于部分条目,是之前没注意到的,单独拉出来点评下。 count(*)和count(1)【强制】不要使用 count(列名)或 count(常量)来替代 count(),count()是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。 官方文档提到,InnoDB下count(*)和count(1)是没有区别的: InnoDB handles SELECT COUNT() and SELECT COUNT(1) operations in the same way. There is no performance difference.但考虑到其他实现对count()有优化(如MyISAM,前提是没有WHERE和GROUP BY子句,直接取缓存的总数),再考虑到用其他DB的情况,统一起见一直用count(*)就好了。更详细的分析可以看 为什么阿里巴巴禁止使用 count(列名)或 count(常量)来替代 count(*) 禁用外键【强制】不得使用外键与级联,一切外键概念必须在应用层解决。说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。 禁止使用外键,在本例中并不是不允许在成绩表中存放student_id字段,只是不设置成为外键即可,更新由应用层来做。 原文地址https://www.cnblogs.com/wuyuegb2312/p/11956714.html

【Java8新特性】一张图带你领略Java8有哪些新特性

【Java8新特性】一张图带你领略Java8有哪些新特性 写在前面很多小伙伴留言说,冰河你能不能写一些关于Java8的文章呢,看书看不下去,看视频进度太慢。好吧,看到不少读者对Java8还是比较陌生的,那我就写一些关于Java8的文章吧,希望对大家有所帮助。至于【高并发专题】,后续咱们会继续更新的。 Java8有哪些新特性? 简单来说,Java8新特性如下所示: Lambda表达式函数式接口方法引用与构造器引用Stream API接口的默认方法与静态方法新时间日期API其他新特性其中,引用最广泛的新特性是Lambda表达式和Stream API。 Java8有哪些优点? 简单来说Java8优点如下所示。 速度更快代码更少(增加了新的语法Lambda表达式)强大的Stream API便于并行最大化减少空指针异常Optional今天就写到这吧,文章虽然很短,但也算是为【Java8新特性】专题开篇了吧,只是从整体上简单说明下Java8有哪些新特性,后续咱们就深入分析这些Java8的新特性! 写在最后如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习Java8新特性。 最后,附上Java8新特性核心知识图,祝大家在学习Java8新特性时少走弯路。 原文地址https://www.cnblogs.com/binghe001/p/12826439.html

C# 9 新特性:代码生成器、编译时反射

C# 9 新特性:代码生成器、编译时反射 前言#今天 .NET 官方博客宣布 C# 9 Source Generators 第一个预览版发布,这是一个用户已经喊了快 5 年特性,今天终于发布了。 简介#Source Generators 顾名思义代码生成器,它允许开发者在代码编译过程中获取查看用户代码并且生成新的 C# 代码参与编译过程,并且可以很好的与代码分析器集成提供 Intellisense、调试信息和报错信息,可以用它来做代码生成,因此也相当于是一个加强版本的编译时反射。 使用 Source Generators,可以做到这些事情: 获取一个 Compilation 对象,这个对象表示了所有正在编译的用户代码,你可以从中获取 AST 和语义模型等信息可以向 Compilation 对象中插入新的代码,让编译器连同已有的用户代码一起编译Source Generators 作为编译过程中的一个阶段执行: 编译运行 -&gt; [分析源代码 -&gt; 生成新代码] -&gt; 将生成的新代码添加入编译过程 -&gt; 编译继续。 上述流程中,中括号包括的内容即为 Source Generators 所参与的阶段和能做到的事情。 作用#.NET 明明具备运行时反射和动态 IL 织入功能,那这个 Source Generators 有什么用呢? 编译时反射 - 0 运行时开销#拿 ASP.NET Core 举例,启动一个 ASP.NET Core 应用时,首先会通过运行时反射来发现 Controllers、Services 等的类型定义,然后在请求管道中需要通过运行时反射获取其构造函数信息以便于进行依赖注入。然而运行时反射开销很大,即使缓存了类型签名,对于刚刚启动后的应用也无任何帮助作用,而且不利于做 AOT 编译。 Source Generators 将可以让 ASP.NET Core 所有的类型发现、依赖注入等在编译时就全部完成并编译到最终的程序集当中,最终做到 0 运行时反射使用,不仅利于 AOT 编译,而且运行时 0 开销。 除了上述作用之外,gRPC 等也可以利用此功能在编译时织入代码参与编译,不需要再利用任何的 MSBuild Task 做代码生成啦! 另外,甚至还可以读取 XML、JSON 直接生成 C# 代码参与编译,DTO 编写全自动化都是没问题的。 AOT 编译#Source Generators 的另一个作用是可以帮助消除 AOT 编译优化的主要障碍。 许多框架和库都大量使用反射,例如System.Text.Json、System.Text.RegularExpressions、ASP.NET Core 和 WPF 等等,它们在运行时从用户代码中发现类型。这些非常不利于 AOT 编译优化,因为为了使反射能够正常工作,必须将大量额外甚至可能不需要的类型元数据编译到最终的原生映像当中。 有了 Source Generators 之后,只需要做编译时代码生成便可以避免大部分的运行时反射的使用,让 AOT 编译优化工具能够更好的运行。 例子#INotifyPropertyChanged#写过 WPF 或 UWP 的都知道,在 ViewModel 中为了使属性变更可被发现,需要实现 INotifyPropertyChanged 接口,并且在每一个需要的属性的 setter 处触发属性更改事件: Copyclass MyViewModel : INotifyPropertyChanged{ public event PropertyChangedEventHandler? PropertyChanged; private string _text; public string Text get =&gt; _text; _text = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text))); }当属性多了之后将会非常繁琐,先前 C# 引入了 CallerMemberName 用于简化属性较多时候的情况: Copyclass MyViewModel : INotifyPropertyChanged{ public event PropertyChangedEventHandler? PropertyChanged; private string _text; public string Text get =&gt; _text; _text = value; OnPropertyChanged(); protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }即,用 CallerMemberName 指示参数,在编译时自动填充调用方的成员名称。 但是还是不方便。 如今有了 Source Generators,我们可以在编译时生成代码做到这一点了。 为了实现 Source Generators,我们需要写个实现了 ISourceGenerator 并且标注了 Generator 的类型。 完整的 Source Generators 代码如下: Copyusing System;using System.Collections.Generic;using System.Linq;using System.Text;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp;using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis.Text; namespace MySourceGenerator{ [Generator] public class AutoNotifyGenerator : ISourceGenerator private const string attributeText = @" using System;namespace AutoNotify{ [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] sealed class AutoNotifyAttribute : Attribute public AutoNotifyAttribute() public string PropertyName { get; set; } public void Initialize(InitializationContext context) // 注册一个语法接收器,会在每次生成时被创建 context.RegisterForSyntaxNotifications(() =&gt; new SyntaxReceiver()); public void Execute(SourceGeneratorContext context) // 添加 Attrbite 文本 context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8)); // 获取先前的语法接收器 if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) return; // 创建处目标名称的属性 CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions; Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options)); // 获取新绑定的 Attribute,并获取INotifyPropertyChanged INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute"); INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged"); // 遍历字段,只保留有 AutoNotify 标注的字段 List&lt;IFieldSymbol&gt; fieldSymbols = new List&lt;IFieldSymbol&gt;(); foreach (FieldDeclarationSyntax field in receiver.CandidateFields) SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree); foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables) // 获取字段符号信息,如果有 AutoNotify 标注则保存 IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol; if (fieldSymbol.GetAttributes().Any(ad =&gt; ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default))) fieldSymbols.Add(fieldSymbol); // 按 class 对字段进行分组,并生成代码 foreach (IGrouping&lt;INamedTypeSymbol, IFieldSymbol&gt; group in fieldSymbols.GroupBy(f =&gt; f.ContainingType)) string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context); context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8)); private string ProcessClass(INamedTypeSymbol classSymbol, List&lt;IFieldSymbol&gt; fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context) if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) // TODO: 必须在顶层,产生诊断信息 return null; string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); // 开始构建要生成的代码 StringBuilder source = new StringBuilder($@" namespace {namespaceName}{{ public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()} // 如果类型还没有实现 INotifyPropertyChanged 则添加实现 if (!classSymbol.Interfaces.Contains(notifySymbol)) source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); // 生成属性 foreach (IFieldSymbol fieldSymbol in fields) ProcessField(source, fieldSymbol, attributeSymbol); source.Append("} }"); return source.ToString(); private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol) // 获取字段名称 string fieldName = fieldSymbol.Name; ITypeSymbol fieldType = fieldSymbol.Type; // 获取 AutoNotify Attribute 和相关的数据 AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad =&gt; ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp =&gt; kvp.Key == "PropertyName").Value; string propertyName = chooseName(fieldName, overridenNameOpt); if (propertyName.Length == 0 || propertyName == fieldName) //TODO: 无法处理,产生诊断信息 return; source.Append($@" public {fieldType} {propertyName} {{ return this.{fieldName}; this.{fieldName} = value; this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName}))); }}"); string chooseName(string fieldName, TypedConstant overridenNameOpt) if (!overridenNameOpt.IsNull) return overridenNameOpt.Value.ToString(); fieldName = fieldName.TrimStart('_'); if (fieldName.Length == 0) return string.Empty; if (fieldName.Length == 1) return fieldName.ToUpper(); return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1); // 语法接收器,将在每次生成代码时被按需创建 class SyntaxReceiver : ISyntaxReceiver public List&lt;FieldDeclarationSyntax&gt; CandidateFields { get; } = new List&lt;FieldDeclarationSyntax&gt;(); // 编译中在访问每个语法节点时被调用,我们可以检查节点并保存任何对生成有用的信息 public void OnVisitSyntaxNode(SyntaxNode syntaxNode) // 将具有至少一个 Attribute 的任何字段作为候选 if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax &amp;&amp; fieldDeclarationSyntax.AttributeLists.Count &gt; 0) CandidateFields.Add(fieldDeclarationSyntax); }有了上述代码生成器之后,以后我们只需要这样写 ViewModel 就会自动生成通知接口的事件触发调用: Copypublic partial class MyViewModel{ [AutoNotify] private string _text = "private field text"; [AutoNotify(PropertyName = "Count")] private int _amount = 5; }上述代码将会在编译时自动生成以下代码参与编译: Copypublic partial class MyViewModel : System.ComponentModel.INotifyPropertyChanged{ public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; public string Text return this._text; this._text = value; this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Text))); public int Count return this._amount; this._amount = value; this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Count))); }非常方便! 使用时,将 Source Generators 部分作为一个独立的 .NET Standard 2.0 程序集(暂时不支持 2.1),用以下方式引入到你的项目即可: 注意需要最新的 .NET 5 preview(写文章时还在 artifacts 里没正式 release),并指定语言版本为 preview: Copypreview另外,Source Generators 需要引入两个 nuget 包: Copy限制#Source Generators 仅能用于访问和生成代码,但是不能修改已有代码,这有一定原因是出于安全考量。 文档#Source Generators 处于早期预览阶段,docs.microsoft.com 上暂时没有相关文档,关于它的文档请访问在 roslyn 仓库中的文档: 后记#目前 Source Generators 仍处于非常早期的预览阶段,API 后期还可能会有很大的改动,因此现阶段不要用于生产。 另外,关于与 IDE 的集成、诊断信息、断点调试信息等的开发也在进行中,请期待后续的 preview 版本吧。 作者: hez2010 出处:https://www.cnblogs.com/hez2010/p/12810993.html

java集合详解

java集合详解 1.java集合是什么?java集合实际上是一种经常被运用到的java类库,其中提供了已经实现的的数据结构,省去了程序员再次编写数据结构的事情.在Leetcode中经常会被用到,有很重要的作用. 我们发现,无论是Set和List都是继承于Collection接口,实现Collection之中的方法,而他们又衍生出了HashSet,LinkedList等等我们经常使用的数据结构. 但是真相并不是如此的简单. 对于Collection接口的实现,其实是由AbstractCollection类完成的. 此类提供了 Collection 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作。 Collection中需要实现的的方法: boolean add(E o) 确保此 collection 包含指定的元素(可选操作)。 boolean addAll(Collection&lt;? extends E&gt; c) 将指定 collection 中的所有元素都添加到此 collection 中(可选操作)。 void clear() 移除此 collection 中的所有元素(可选操作)。 boolean contains(Object o) 如果此 collection 包含指定的元素,则返回 true。 boolean containsAll(Collection&lt;?&gt; c) 如果此 collection 包含指定 collection 中的所有元素,则返回 true。 boolean equals(Object o) 比较此 collection 与指定对象是否相等。 int hashCode() 返回此 collection 的哈希码值。 boolean isEmpty() 如果此 collection 不包含元素,则返回 true。 Iterator iterator() 返回在此 collection 的元素上进行迭代的迭代器。 boolean remove(Object o) 从此 collection 中移除指定元素的单个实例,如果存在的话(可选操作)。 boolean removeAll(Collection&lt;?&gt; c) 移除此 collection 中那些也包含在指定 collection 中的所有元素(可选操作)。 boolean retainAll(Collection&lt;?&gt; c) 仅保留此 collection 中那些也包含在指定 collection 的元素(可选操作)。 int size() 返回此 collection 中的元素数。 Object[] toArray() 返回包含此 collection 中所有元素的数组。 T[] toArray(T[] a) 返回包含此 collection 中所有元素的数组;返回数组的运行时类型与指定数组的运行时类型相同。 AbstractCollection类实现的方法: boolean add(E o) 确保此 collection 包含指定的元素(可选操作)。 boolean addAll(Collection&lt;? extends E&gt; c) 将指定 collection 中的所有元素添加到此 collection 中(可选操作)。 void clear() 从此 collection 中移除所有元素(可选操作)。 boolean contains(Object o) 如果此 collection 包含指定的元素,则返回 true。 boolean containsAll(Collection&lt;?&gt; c) 如果此 collection 包含指定 collection 中的所有元素,则返回 true。 boolean isEmpty() 如果此 collection 不包含元素,则返回 true。 abstract Iterator iterator() 返回在此 collection 中的元素上进行迭代的迭代器。 boolean remove(Object o) 从此 collection 中移除指定元素的单个实例(如果存在)(可选操作)。 boolean removeAll(Collection&lt;?&gt; c) 从此 collection 中移除包含在指定 collection 中的所有元素(可选操作)。 boolean retainAll(Collection&lt;?&gt; c) 仅在此 collection 中保留指定 collection 中所包含的元素(可选操作)。 abstract int size() 返回此 collection 中的元素数。 Object[] toArray() 返回包含此 collection 中所有元素的数组。 T[] toArray(T[] a) 返回包含此 collection 中所有元素的数组;返回数组的运行时类型是指定数组的类型。 String toString() 返 出了一个hashcode方法,AbstractCollection类实现了几乎所有的功能. 而AbstractCollection类又有三个不同的子类AbstractList, AbstractQueue,AbstractSet.我们从名字就可以知道,这就是三种不同的数据结构.于是这样基本就可以分析出来. 集合类的构建框架如下. 所有的集合都是依靠这种方式构建的,用一个抽象类实现接口,然后再用集合类去实现这些抽象类,来完成构建集合的目的. 这是完整的构建图. 这其实是为了大家有一个思想,就是在Collection实现的方法,在继承实现他的各个集合中也都会实现. 如下是本文的目录: (一) Iterator接口--迭代器{ boolean hasNext() 如果仍有元素可以迭代,则返回 true。 E next() 返回迭代的下一个元素。 void remove() 删除 default void forEach 实现了迭代器接口的类才可以使用forEach } 这几个方法有着很重要的意义: 所有实现Collection接口(也就是大部分集合)都可以使用forEach功能.通过反复调用next()方法可以访问集合内的每一个元素.java迭代器查找的唯一操作就是依靠调用next,而在执行查找任务的同时,迭代器的位置也在改变.Iterator迭代器remove方法会删除上次调用next方法返回的元素.这也意味之remove方法和next有着很强的依赖性.如果在调用remove之前没有调用next是不合法的.这个接口衍生出了,java集合的迭代器. java集合的迭代器使用 下面是迭代器的一个小栗子: class test { public static void run() { List list = new LinkedList&lt;&gt;(); list.add(1); list.add(2); list.add(3); list.add(3); Iterator iterator = list.iterator();//依靠这个方法生成一个java集合迭代器&lt;--在Colletion接口中的方法,被所有集合实现. // iterator.remove();报错java.lang.IllegalStateException iterator.next(); iterator.remove();//不报错,删掉了1 System.out.println(list);//AbstractCollection类中实现的toString让list可以直接被打印出来 while (iterator.hasNext()) {//迭代器,hasNext()集合是否为空 Integer a = iterator.next();//可以用next访问每一个元素 System.out.println(a);//2,3 ,3 } for (int a : list) {//也可以使用foreach System.out.println(a);//2,3,3 } } public static void main(String[] args) { run(); } } 当然你也会有点好奇,为什么remove方法前面必须跟着一个next方法.其实这个只能这么解释. 迭代器的next方法的运行方式并不是类似于数组的运行方式. 当然,这张图主要是让你理解一下. 数组的指针指向要操作的元素上面,而迭代器却是将要操作的元素放在运动轨迹中间. 本质来讲,迭代器的指针并不是指在元素上,而是指在元素和元素中间. 假设现在调用remove().被删除的就是2号元素.(被迭代器那个圆弧笼盖在其中的那个元素).如果再次调用,就会报错,因为圆弧之中的2号元素已经消失,那里是空的,无法删除. (二) Collection接口这个对象是一个被LIst,Set,Queue的超类, 这个接口中的方法,构成了集合中主要的方法和内容.剩下的集合往往都是对这个接口的扩充. 方法如下: boolean add(E o) 确保此 collection 包含指定的元素(可选操作)。 boolean addAll(Collection&lt;? extends E&gt; c) 将指定 collection 中的所有元素添加到此 collection 中(可选操作)。 void clear() 从此 collection 中移除所有元素(可选操作)。 boolean contains(Object o) 如果此 collection 包含指定的元素,则返回 true。 boolean containsAll(Collection&lt;?&gt; c) 如果此 collection 包含指定 collection 中的所有元素,则返回 true。 boolean isEmpty() 如果此 collection 不包含元素,则返回 true。 abstract Iterator iterator() 返回在此 collection 中的元素上进行迭代的迭代器。 boolean remove(Object o) 从此 collection 中移除指定元素的单个实例(如果存在)(可选操作)。 boolean removeAll(Collection&lt;?&gt; c) 从此 collection 中移除包含在指定 collection 中的所有元素(可选操作)。 boolean retainAll(Collection&lt;?&gt; c) 仅在此 collection 中保留指定 collection 中所包含的元素(可选操作)。 abstract int size() 返回此 collection 中的元素数。 Object[] toArray() 返回包含此 collection 中所有元素的数组。 T[] toArray(T[] a) 返回包含此 collection 中所有元素的数组;返回数组的运行时类型是指定数组的类型。 String toString() &lt;--很重要 返 其实我们并不一定要把这些方法都记住 我们只要记住Collection对象实现了这些种类的方法就可以了(可以现查API,不是.. 但是确实,这些方法记住了有很大的用处. 添加元素(两种) 添加一个元素,添加一个集合 删除元素(三种) 删除一个元素,删出一个集合,只保留一个集合 判断大小 变成数组 是否为空 清空 java集合的泛型使用 到这里我们还要讲解一个问题,就是除了Map的集合类型(看看上面的继承表,map是单独一个分支)都可以传入Collection为参数的函数里面去. public class Test { public static void display(Collection&lt;?&gt; a){ System.out.println(a); } public static void main(String[] args) { List list=new LinkedList&lt;&gt;();//链表 list.add(1); list.add(2); list.add(4); list.add(3); display(list);//[1, 2, 4, 3] Set set=new TreeSet&lt;&gt;();//树集 set.addAll(list); //在这里之所以两者输出不同,是因为树集有着一个自动排序的功能.其原因在于对于treeset内部结构的实现和LinkedList有所不同 display(set);//[1, 2, 3, 4] } } java集合中使用泛型的好处 为什么在java集合中经常使用泛型.除了为了防止输入错误的数据,更重要的是如果用了泛型也会让操作更加的方便,省去了强制转化的过程. 以下两个是准备 public class AppleAndOrangeWithOutGenerics { @SuppressWarnings("unchecked")//这个只能抑制警告信息,用它就不会有警告 public static void main(String args[]) { / 不用泛型 / // ArrayList apples=new ArrayList(); // for (int i = 0; i &lt;3 ; i++) { // apples.add(new Apple()); //在ArrayList无论放进去之前是什么,再放进去之后都会变成Object类型, // apples.add(new Orange()); //会报一个小小的warning,因为没有使用泛型.&lt;-只有删掉这个句子执行才不报错 // } // for (int j = 0; j 使用泛型 / ArrayList apples = new ArrayList(); for (int i = 0; i &lt; 3; i++) { apples.add(new Apple()); // apples.add(new Orange());//在这里直接就报错了,让这种错误在编译期就被发现 } for (int j = 0; j &lt; apples.size(); j++) { //用了反省之后连强制转换都不需要了 System.out.println(( apples.get(j)).id());//如果没有泛型的拦截,输入Orange类型根本不会被发现.非常的危险 } } } 所以使用泛型有很大的好处. (三) ListList是一个有序集合,元素会增加到容器中特定的位置,可以采用两种方式访问元素:使用迭代器访问或者使用一个整数索引访问.后一种方式称为随机访问. 为此List接口多定义了一些方法,来实现这一点 void add(int index,E element);//这个和上面不同,带了index. void remove(int index); E get(int index); E set(int index,E element); 我们知道实现LIST接口的类中有一个类叫做AbstractList,他的两个子类分别是LinkedList和ArrayList这两种.那么问题是链表可不可以使用这个add方法. 答案是可以的.实际上链表使用随机访问,只不过是慢了点而已.如果有可能,还是使用迭代器为好. LIST主要有两种类.一个是LinkedList一个是ArrayList. LinkedList我们就从一个程序看一看LinkedList到底怎么用. /* LinkedLIST也像ArrayList一扬实现了基本的List接口,但是他执行一些操作效率更高,比如插入. LIST为空就会抛出NoSuchElement-Exception Created by 22643 on 2020/4/17. */ public class LinkedListFeatures { public static void main(String[] args) { LinkedList pets=new LinkedList(Pets.arrayList(5));//后面括号中的意思是生成五个Pets对象 System.out.println("pets = [" + pets + "]"); System.out.println("pets.getFirst() "+pets.getFirst());//取第一个 System.out.println("pets.element "+pets.element());//也是取第一个,跟first完全相同. //如果列表为空如上两个内容返回NoSuchElement-Exception System.out.println("pets.peek()"+pets.peek());//peek跟上面两个一扬,只是在列表为空的时候返回null System.out.println(pets.removeFirst()); System.out.println(pets.remove());//这两个完全一样,都是移除并返回列表头,列表空的时候返回NoSuchElement-Exception System.out.println(pets.poll());//稍有差异,列表为空的时候返回null pets.addFirst(new Rat());//addFirst将一个元素插入头部,addLast是插到尾部 System.out.println(pets); pets.add(new Rat());//将一个元素插入尾部 System.out.println(pets); pets.offer(new Rat());//与上面一扬,将一个元素插入尾部 System.out.println(pets); pets.set(2,new Rat());//将替换为指定的元素 System.out.println(pets); } } 实际上LinkedList有非常多的方法,因为LinkedList是被用来实现多中数据结构的.不但可以实现队列,甚至还有可以实现栈的相关方法. 我们对此进行分类: 栈相关的操作方法: E poll() 找到并移除此列表的头(第一个元素)。 peek() 找到但不移除此列表的头(第一个元素)。 void addFirst(E o) 加入开头可以当作add用 队列操作方法:(LinkedList实现了Queue的接口,所以说可以操作用来构建队列) 注意队列是FIFO(先进先出)队列,所以按照实现,从普通队列是从队列的尾部插入,从头部移除,. 所以方法如下: E element() 首元素 boolean offer(E o)将指定队列插入桶 E peek() 检索,但是不移除队列的头 E pool()检索并移除此队列的头,为空返回null. E remove()检索并移除此队列的头 一般来讲集合中的方法在移除方法都会有一个为空的时候返回null的方法,和一个为空的时候返回null的方法.类似于pool()和remove() 我们一会到Queue的时候还会将这些再将一次. ArrayList我们也从一个程序来看这个 public class ListFeatures { public static void main(String[] args) { Random rand=new Random(47);//相同的种子会产生相同的随机序列。 List list=new ArrayList&lt;&gt;(); list.add("demo3"); list.add("demo2"); list.add("demo1");//加入方法 System.out.println("插入元素前list集合"+list);//可以直接输出 / /void add(int index, E element)在指定位置插入元素,后面的元素都往后移一个元素 / list.add(2,"demo5"); System.out.println("插入元素前list集合"+list); List listtotal=new ArrayList&lt;&gt;(); List list1=new ArrayList&lt;&gt;(); List list2=new ArrayList&lt;&gt;(); list1.add("newdemo1"); list1.add("newdemo2"); list1.add("newdemo2"); / boolean addAll(int index, Collection&lt;? extends E&gt; c) 在指定的位置中插入c集合全部的元素,如果集合发生改变,则返回true,否则返回false。 意思就是当插入的集合c没有元素,那么就返回false,如果集合c有元素,插入成功,那么就返回true。 / boolean b=listtotal.addAll(list1); boolean c=listtotal.addAll(2,list2); System.out.println(b); System.out.println(c);//插入2号位置,list2是空的 System.out.println(list1); / E get(int index) 返回list集合中指定索引位置的元素 / System.out.println(list1.get(1));//list的下标是从0开始的 / int indexOf(Object o) 返回list集合中第一次出现o对象的索引位置,如果list集合中没有o对象,那么就返回-1 / System.out.println(list1.indexOf("demo")); System.out.println(list1.indexOf("newdemo2")); //如果在list中有相同的数,也没有问题. //但是如果是对象,因为每个对象都是独一无二的.所以说如果传入一个新的对象,indexof和remove都是无法完成任务的 //要是删除,可以先找到其位置,然后在进行删除. //Pet p=pets.get(2); //pets.remove(p); / 查看contains查看参数是否在list中 / System.out.println(list1.contains("newdemo2"));//true / remove移除一个对象 返回true和false / //只删除其中的一个 System.out.println(list1.remove("newdemo2"));//[newdemo1, newdemo2] System.out.println(list1); List pets=list1.subList(0,1);//让你从较大的一个list中创建一个片段 //containall一个list在不在另一个list中 System.out.println(pets+"在list中嘛"+list1.containsAll(pets));//[newdemo1]在list中嘛true //因为sublist的背后就是初始化列表,所以对于sublist的修改会直接反映到原数组上面 pets.add("new add demo"); System.out.println(list1);//[newdemo1, new add demo, newdemo2] Collections.sort(pets); System.out.println( pets );//new add demo, newdemo1 System.out.println(list1.containsAll(pets));//true-----变换位置不会影响是否在list1中被找到. list1.removeAll(pets);//移除在参数list中的全部数据 / list1[newdemo1, new add demo, newdemo2] pets[new add demo, newdemo1] / System.out.println(list1);//[newdemo2] System.out.println(list1.isEmpty());//是否为空 System.out.println(list1.toArray());//将list变为数组 //list的addAll方法有一个重载的.可以让他在中间加入 } } 这个比较适合非顺序存储. (四)(五)SetSet实际上也是一种映射关系的集合和Map比较像.但是它实现的依然是Collection的接口. 而且Set中的方法和Collection的方法几乎完全一样. 唯一的区别在于add方法不允许增加重复的元素.在调用equal时,如果两个Set中的元素都相等,无论两者的顺序如何,这两个Set都会相等. set的特性 Set不保存重复的元素. Set就是Collection,只是行为不同. HashSet使用了散列,它打印的时候,输出的元素不会正常排列 TreeSet使用了储存在红黑树结构中,,所以输出的元素会正常排列 当然Set最主要的工作就是判断存在性,目的是看一个元素到底存不存在这个集合之中. 下面放上两个Set的例子: SortedSet(TreeSet) public class SortedSetOfInteger { public static void main(String[] args) { Random random=new Random(47); SortedSet intset=new TreeSet&lt;&gt;(); for (int i = 0; i &lt;100 ; i++) { intset.add(random.nextInt(30)); } System.out.println(intset);//set特性只能输入相同的数,别看输入了100个数,但是实际上只有30个进去了. //这个有序了.这就是treeset的功劳,因为内部的实现时红黑树,所以来说.这就简单了一些 } } HashSet public class SetOfInteger { public static void main(String[] args) { Random rand=new Random(47); Set intset=new HashSet&lt;&gt;();//创建一个HashSet for (int i = 0; i &lt;100 ; i++) { intset.add(rand.nextInt(30)); } System.out.println(intset);//set特性只能输入相同的数,别看输入了100个数,但是实际上只有30个进去了. } } 这里要讲一下HashSet。HashSet不在意元素的顺序,根据属性可以快速的访问所需要的对象。散列表为每个对象计算一个整数,成为散列码...散列码是实例产生的一个整数。 散列表(HashSet)散列表用链表数组实现。每个列表称为通。想要查找表中对象的位置就计算它的散列码。然后与通的总数取余,得到的数就是保存这个元素的通的索引。 但是桶总有被沾满的一刻。 为了应对这种情况,需要用新对象与桶中所有对象比较,看是否存在。 为了控制性能就要能定义初始桶数,设置为要插入元素的75%-150%,最好为素数。 这个时候就要执行再散列,让这个散列表获得更多的内容。 需要创建一个桶数更多的表,并将全部元素插入这个新表中。装填因子绝对什么时候在执行,如果装填因子为0.75那么就是在表中75%的位置被沾满时,表会给如双倍的桶数自动散列。 Queue队列是数据结构中比较重要的一种类型,它支持 FIFO,尾部添加、头部删除(先进队列的元素先出队列),跟我们生活中的排队类似。 但是在集合中的Queue并没有单独的实现类,而是用LinkedList实现的。其实你只要看一眼LinkedList的方法就知道,他完全可以实现队列的操作。 add()尾部添加 removeFirst()删除头部元素 peek()查看头部元素 Queue主要有两种不同的类型. 分别是优先级队列和Deque队列 PriorityQueue 优先级队列中元素可以按照任意的顺序插入,却按照目标排序的顺序进行检索,也就是无论什么时候调用remove移除的都是当前最小的元素。 优先级使用了一种堆,一个可以自我调节的二叉树,对树进行执行添加和删除。它可以让最小的元素移动到跟,而不必花时间对其排序。 当然,你也可以自己对其进行排序. import java.text.DecimalFormat; import java.util.Comparator; import java.util.PriorityQueue; import java.util.Queue; / @Author:sks @Description: @Date:Created in 10:39 2018/1/11 @Modified by: / //二维平面上一个点 class point { //坐标x double x; //坐标y double y; public point(double x, double y){ this.x = x; this.y = y; } } //排序函数 class PointComparator { private point pointOne; private point pointTwo; public double distance; public PointComparator(point pointOne,point pointTwo) { this.pointOne = pointOne; this.pointTwo = pointTwo; computeDistance(); } //计算两点之间距离 private void computeDistance() { double val = Math.pow((this.pointOne.x - this.pointTwo.x),2) + Math.pow((this.pointOne.y - this.pointTwo.y),2); this.distance = Math.sqrt(val); } } public class PriorityQueuep_test { public static void main(String args[]){ Comparator OrderDistance = new Comparator(){ public int compare(PointComparator one, PointComparator two) { if (one.distance &lt; two.distance) return 1; else if (one.distance &gt; two.distance) return -1; else return 0; } }; //定义一个优先队列,用来排序任意两点之间的距离,从大到小排 Queue FsQueue = new PriorityQueue(10,OrderDistance); for (int i=0;i&lt;6;i++){ java.util.Random r= new java.util.Random(10); point one =new point(i2+1,i3+2); point two =new point(i5+2,i6+3); PointComparator nodecomp = new PointComparator(one,two); DecimalFormat df = new DecimalFormat("#.##"); FsQueue.add(nodecomp); } DecimalFormat df = new DecimalFormat("#.###"); for (int i = 0;i&lt;6;i++){ System.out.println(df.format(FsQueue.poll().distance)); } } } Deque deque也有些复杂,它可以用ArrayDeque实现,也可以用LinkedList实现. 线性集合,支持两端的元素插入和移除。Deque是double ended queue的简称,习惯上称之为双端队列。大多数Deque 实现对它们可能包含的元素的数量没有固定的限制,但是该接口支持容量限制的deques以及没有固定大小限制的deque。 作者:我是吸血鬼链接:https://www.jianshu.com/p/d78a7c982edb来源:简书著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 因为本身也是LinkedList实现的,所以其本身的方法和LinkedList差不了多少. public class Main { public static void main(String[] args) { Deque deque = new LinkedList&lt;&gt;(); deque.offerLast("A"); // A deque.offerLast("B"); // B -&gt; A deque.offerFirst("C"); // B -&gt; A -&gt; C System.out.println(deque.pollFirst()); // C, 剩下B -&gt; A System.out.println(deque.pollLast()); // B System.out.println(deque.pollFirst()); // A System.out.println(deque.pollFirst()); // null } } Stackstack的名字大家都知道,就是栈.一个先进后出的数据结构,这里我并不认为应该使用java集合中提供的栈集合. 而是应该使用LinkedList来构建集合:一个小任务: 用LinkedList实现栈 public class Stack { private LinkedList storage=new LinkedList&lt;&gt;();//用LinkedList作为栈的核心 public void push(T v){ storage.addFirst(v);}// public T peek(){ return storage.getFirst();} public T pop(){return storage.removeFirst();} public boolean empty(){return storage.isEmpty();} public String toString(){return storage.toString();} } 这样做有一个好处,就是这样的栈可以有更多种的方法,可以采用更多种的方式.无疑这样的栈会更好一些. 所以我推荐大家用栈的时候,用LinkedList来实现. MAP讲了Collection接口实现的各种集合,我们就要讲讲非Collection的集合.这意味着你在Collection中记住的方法在这个里面完全用不到了. 我们知道一些键的信息,想要知道与之对应的元素.映射结构就是为此设计的,映射用来存放键值对,如提供了键就能查到值. 和Set一样,HashMap要比TreeMap要快上一些,但是TreeMap有序.这与Set很相似,毕竟Set其实也是映射结构. 每当往映射加入对象是,必须同时提供一个键. 键是唯一的,不能对同一个键放两个值.如果对同一个键调用两次put方法,第二次调用会取代第一个. 要想处理所有的键和值,那就应该使用foreach 列子如下: public class MapOfList { public static Map&gt; people=new HashMap&lt;&gt;(); static { people.put(new Person("dawn"), Arrays.asList(new Cymric("Molly"),new Mutt("Spot"))); //就写一个了,有点懒了. } public static void main(String[] args) { System.out.println(people.keySet());//返回的是键组成的set System.out.println(people.values());//返回的时值组成的set for (Person person:people.keySet() ) { System.out.println(person+"has :"); for (Pet pet:people.get(person) ) { System.out.println(pet); } } } } 下面还有一个HashMap使用的例子 public class PetMap { public static void main(String[] args) { Map petMap=new HashMap(); petMap.put("My Cat",new Cat("MALL")); petMap.put("My Dog",new Dog("DOGGY")); petMap.put("My Haster",new Hamster("Bosco")); System.out.println(petMap); Pet dog=petMap.get("My Dog"); System.out.println(dog); System.out.println(petMap.containsKey("My Dog"));// System.out.println(petMap.containsValue(dog));// } } 本文作者:Timothy Rasinski本文链接:https://www.cnblogs.com/yanzezhong/p/12808089.html

C#多线程(15):任务基础③

C#多线程(15):任务基础③ 目录TaskAwaiter延续的另一种方法另一种创建任务的方法实现一个支持同步和异步任务的类型Task.FromCanceled()如何在内部取消任务Yield 关键字补充知识点任务基础一共三篇,本篇是第三篇,之后开始学习异步编程、并发、异步I/O的知识。 本篇会继续讲述 Task 的一些 API 和常用的操作。 TaskAwaiter先说一下 TaskAwaiter,TaskAwaiter 表示等待异步任务完成的对象并为结果提供参数。 Task 有个 GetAwaiter() 方法,会返回TaskAwaiter 或TaskAwaiter,TaskAwaiter 类型在 System.Runtime.CompilerServices 命名空间中定义。 TaskAwaiter 类型的属性和方法如下: 属性 说明IsCompleted 获取一个值,该值指示异步任务是否已完成。方法: 方法 说明GetResult() 结束异步任务完成的等待。OnCompleted(Action) 将操作设置为当 TaskAwaiter 对象停止等待异步任务完成时执行。UnsafeOnCompleted(Action) 计划与此 awaiter 相关异步任务的延续操作。使用示例如下: static void Main() Task&lt;int&gt; task = new Task&lt;int&gt;(()=&gt; Console.WriteLine("我是前驱任务"); Thread.Sleep(TimeSpan.FromSeconds(1)); return 666; TaskAwaiter&lt;int&gt; awaiter = task.GetAwaiter(); awaiter.OnCompleted(()=&gt; Console.WriteLine("前驱任务完成时,我就会继续执行"); task.Start(); Console.ReadKey(); 另外,我们前面提到过,任务发生未经处理的异常,任务被终止,也算完成任务。 延续的另一种方法上一节我们介绍了 .ContinueWith() 方法来实现延续,这里我们介绍另一个延续方法 .ConfigureAwait()。 .ConfigureAwait() 如果要尝试将延续任务封送回原始上下文,则为 true;否则为 false。 我来解释一下, .ContinueWith() 延续的任务,当前驱任务完成后,延续任务会继续在此线程上继续执行。这种方式是同步的,前者和后者连续在一个线程上运行。 .ConfigureAwait(false) 方法可以实现异步,前驱方法完成后,可以不理会后续任务,而且后续任务可以在任意一个线程上运行。这个特性在 UI 界面程序上特别有用。 可以参考:https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f 其使用方法如下: static void Main() Task&lt;int&gt; task = new Task&lt;int&gt;(()=&gt; Console.WriteLine("我是前驱任务"); Thread.Sleep(TimeSpan.FromSeconds(1)); return 666; ConfiguredTaskAwaitable&lt;int&gt;.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter(); awaiter.OnCompleted(()=&gt; Console.WriteLine("前驱任务完成时,我就会继续执行"); task.Start(); Console.ReadKey(); ConfiguredTaskAwaitable.ConfiguredTaskAwaiter 拥有跟 TaskAwaiter 一样的属性和方法。 .ContinueWith() 跟 .ConfigureAwait(false) 还有一个区别就是 前者可以延续多个任务和延续任务的任务(多层)。后者只能延续一层任务(一层可以有多个任务)。 另一种创建任务的方法前面提到提到过,创建任务的三种方法:new Task()、Task.Run()、Task.Factory.SatrtNew(),现在来学习第四种方法:TaskCompletionSource 类型。 我们来看看 TaskCompletionSource 类型的属性和方法: 属性 说明Task 获取由此 Task 创建的 TaskCompletionSource。方法: 方法 说明SetCanceled() 将基础 Task 转换为 Canceled 状态。SetException(Exception) 将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。SetException(IEnumerable) 将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。SetResult(TResult) 将基础 Task 转换为 RanToCompletion 状态。TrySetCanceled() 尝试将基础 Task 转换为 Canceled 状态。TrySetCanceled(CancellationToken) 尝试将基础 Task 转换为 Canceled 状态并启用要存储在取消的任务中的取消标记。TrySetException(Exception) 尝试将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。TrySetException(IEnumerable) 尝试将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。TrySetResult(TResult) 尝试将基础 Task 转换为 RanToCompletion 状态。TaskCompletionSource 类可以对任务的生命周期做控制。 首先要通过 .Task 属性,获得一个 Task 或 Task 。 TaskCompletionSource&lt;int&gt; task = new TaskCompletionSource&lt;int&gt;(); Task&lt;int&gt; myTask = task.Task; // Task myTask = task.Task; 然后通过 task.xxx() 方法来控制 myTask 的生命周期,但是呢,myTask 本身是没有任务内容的。 使用示例如下: static void Main() TaskCompletionSource&lt;int&gt; task = new TaskCompletionSource&lt;int&gt;(); Task&lt;int&gt; myTask = task.Task; // task 控制 myTask // 新开一个任务做实验 Task mainTask = new Task(() =&gt; Console.WriteLine("我可以控制 myTask 任务"); Console.WriteLine("按下任意键,我让 myTask 任务立即完成"); Console.ReadKey(); task.SetResult(666); mainTask.Start(); Console.WriteLine("开始等待 myTask 返回结果"); Console.WriteLine(myTask.Result); Console.WriteLine("结束"); Console.ReadKey(); 其它例如 SetException(Exception) 等方法,可以自行探索,这里就不再赘述。 参考资料:https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/ 这篇文章讲得不错,而且有图:https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/ 实现一个支持同步和异步任务的类型这部分内容对 TaskCompletionSource 继续进行讲解。 这里我们来设计一个类似 Task 类型的类,支持同步和异步任务。 用户可以使用 GetResult() 同步获取结果;用户可以使用 RunAsync() 执行任务,使用 .Result 属性异步获取结果;其实现如下: /// /// 实现同步任务和异步任务的类型/// /// public class MyTaskClass{ private readonly TaskCompletionSource&lt;TResult&gt; source = new TaskCompletionSource&lt;TResult&gt;(); private Task&lt;TResult&gt; task; // 保存用户需要执行的任务 private Func&lt;TResult&gt; _func; // 是否已经执行完成,同步或异步执行都行 private bool isCompleted = false; // 任务执行结果 private TResult _result; /// &lt;summary&gt; /// 获取执行结果 /// &lt;/summary&gt; public TResult Result if (isCompleted) return _result; else return task.Result; public MyTaskClass(Func&lt;TResult&gt; func) _func = func; task = source.Task; /// &lt;summary&gt; /// 同步方法获取结果 /// &lt;/summary&gt; /// &lt;returns&gt;&lt;/returns&gt; public TResult GetResult() _result = _func.Invoke(); isCompleted = true; return _result; /// &lt;summary&gt; /// 异步执行任务 /// &lt;/summary&gt; public void RunAsync() Task.Factory.StartNew(() =&gt; source.SetResult(_func.Invoke()); isCompleted = true; }我们在 Main 方法中,创建任务示例: class Program static void Main() // 实例化任务类 MyTaskClass&lt;string&gt; myTask1 = new MyTaskClass&lt;string&gt;(() =&gt; Thread.Sleep(TimeSpan.FromSeconds(1)); return "www.whuanle.cn"; // 直接同步获取结果 Console.WriteLine(myTask1.GetResult()); // 实例化任务类 MyTaskClass&lt;string&gt; myTask2 = new MyTaskClass&lt;string&gt;(() =&gt; Thread.Sleep(TimeSpan.FromSeconds(1)); return "www.whuanle.cn"; // 异步获取结果 myTask2.RunAsync(); Console.WriteLine(myTask2.Result); Console.ReadKey(); Task.FromCanceled()微软文档解释:创建 Task,它因指定的取消标记进行的取消操作而完成。 这里笔者抄来了一个示例: var token = new CancellationToken(true);Task task = Task.FromCanceled(token);Task genericTask = Task.FromCanceled(token);网上很多这样的示例,但是,这个东西到底用来干嘛的?new 就行了? 带着疑问我们来探究一下,来个示例: public static Task Test() CancellationTokenSource source = new CancellationTokenSource(); source.Cancel(); return Task.FromCanceled&lt;object&gt;(source.Token); static void Main() var t = Test(); // 在此设置断点,监控变量 Console.WriteLine(t.IsCanceled); Task.FromCanceled() 可以构造一个被取消的任务。我找了很久,没有找到很好的示例,如果一个任务在开始前就被取消,那么使用 Task.FromCanceled() 是很不错的。 这里有很多示例可以参考:https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken)/ 如何在内部取消任务之前我们讨论过,使用 CancellationToken 取消令牌传递参数,使任务取消。但是都是从外部传递的,这里来实现无需 CancellationToken 就能取消任务。 我们可以使用 CancellationToken 的 ThrowIfCancellationRequested() 方法抛出 System.OperationCanceledException 异常,然后终止任务,任务会变成取消状态,不过任务需要先传入一个令牌。 这里笔者来设计一个难一点的东西,一个可以按顺序执行多个任务的类。 示例如下: /// &lt;summary&gt; /// 能够完成多个任务的异步类型 /// &lt;/summary&gt; public class MyTaskClass private List&lt;Action&gt; _actions = new List&lt;Action&gt;(); private CancellationTokenSource _source = new CancellationTokenSource(); private CancellationTokenSource _sourceBak = new CancellationTokenSource(); private Task _task; /// &lt;summary&gt; /// 添加一个任务 /// &lt;/summary&gt; /// &lt;param name="action"&gt;&lt;/param&gt; public void AddTask(Action action) _actions.Add(action); /// &lt;summary&gt; /// 开始执行任务 /// &lt;/summary&gt; /// &lt;returns&gt;&lt;/returns&gt; public Task StartAsync() // _ = new Task() 对本示例无效 _task = Task.Factory.StartNew(() =&gt; for (int i = 0; i &lt; _actions.Count; i++) int tmp = i; Console.WriteLine($"第 {tmp} 个任务"); if (_source.Token.IsCancellationRequested) Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("任务已经被取消"); Console.ForegroundColor = ConsoleColor.White; _sourceBak.Cancel(); _sourceBak.Token.ThrowIfCancellationRequested(); _actions[tmp].Invoke(); },_sourceBak.Token); return _task; /// &lt;summary&gt; /// 取消任务 /// &lt;/summary&gt; /// &lt;returns&gt;&lt;/returns&gt; public Task Cancel() _source.Cancel(); // 这里可以省去 _task = Task.FromCanceled&lt;object&gt;(_source.Token); return _task; Main 方法中: static void Main() // 实例化任务类 MyTaskClass myTask = new MyTaskClass(); for (int i = 0; i &lt; 10; i++) int tmp = i; myTask.AddTask(() =&gt; Console.WriteLine(" 任务 1 Start"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine(" 任务 1 End"); Thread.Sleep(TimeSpan.FromSeconds(1)); // 相当于 Task.WhenAll() Task task = myTask.StartAsync(); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine($"任务是否被取消:{task.IsCanceled}"); // 取消任务 Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("按下任意键可以取消任务"); Console.ForegroundColor = ConsoleColor.White; Console.ReadKey(); var t = myTask.Cancel(); // 取消任务 Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"任务是否被取消:【{task.IsCanceled}】"); Console.ReadKey(); 你可以在任一阶段取消任务。 Yield 关键字迭代器关键字,使得数据不需要一次性返回,可以在需要的时候一条条迭代,这个也相当于异步。 迭代器方法运行到 yield return 语句时,会返回一个 expression,并保留当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。 可以使用 yield break 语句来终止迭代。 官方文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield 网上的示例大多数都是 foreach 的,有些同学不理解这个到底是啥意思。笔者这里简单说明一下。 我们也可以这样写一个示例: 这里已经没有 foreach 了。 private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; private static IEnumerable&lt;int&gt; ForAsync() int i = 0; while (i &lt; list.Length) yield return list[i]; 但是,同学又问,这个 return 返回的对象 要实现这个 IEnumerable 才行嘛?那些文档说到什么迭代器接口什么的,又是什么东西呢? 我们可以先来改一下示例: private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; private static IEnumerable&lt;int&gt; ForAsync() int i = 0; while (i &lt; list.Length) int num = list[i]; yield return num; 你在 Main 方法中调用,看看是不是正常运行? static void Main() foreach (var item in ForAsync()) Console.WriteLine(item); Console.ReadKey(); 这样说明了,yield return 返回的对象,并不需要实现 IEnumerable 方法。 其实 yield 是语法糖关键字,你只要在循环中调用它就行了。 static void Main() foreach (var item in ForAsync()) Console.WriteLine(item); Console.ReadKey(); private static IEnumerable&lt;int&gt; ForAsync() int i = 0; while (i &lt; 100) yield return i; 它会自动生成 IEnumerable ,而不需要你先实现 IEnumerable 。 补充知识点线程同步有多种方法:临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphores)、事件(Event)、任务(Task);Task.Run() 和 Task.Factory.StartNew() 封装了 Task;Task.Run()是 Task.Factory.StartNew() 的简化形式;有些地方 net Task() 是无效的;但是 Task.Run() 和 Task.Factory.StartNew() 可以;本篇是任务基础的终结篇,至此 C# 多线程系列,一共完成了 15 篇,后面会继续深入多线程和任务的更多使用方法和场景。 原文地址https://www.cnblogs.com/whuanle/p/12802943.html

python 异步Web框架sanic

python 异步Web框架sanic 我们继续学习Python异步编程,这里将介绍异步Web框架sanic,为什么不是tornado?从框架的易用性来说,Flask要远远比tornado简单,可惜flask不支持异步,而sanic就是类似Flask语法的异步框架。 github:https://github.com/huge-success/sanic 不过sanic对环境有要求: macOS/linuxpython 3.6+不过,我在macOS上安装 sanic 还是踩了坑。依赖库ujson一直安装失败。最后不得不卸载官方python,安装 miniconda(第三方Python安装包,集成了一些额外的工具)。 安装 sanic pip3 install sanicsanic 开发第一个例子 编写官方的第一个例子hello.py: from sanic import Sanicfrom sanic.response import jsonfrom sanic.exceptions import NotFound app = Sanic(name="pyapp") @app.route('/')async def test(request): return json({'hello': 'world'}) if name == '__main__': app.error_handler.add( NotFound, lambda r, e: sanic.response.empty(status=404) app.run(host='0.0.0.0', port=8000) 运行上面的程序: python3 hello.py [2020-04-21 23:12:02 +0800] [18487] [INFO] Goin Fast @ http://0.0.0.0:8000[2020-04-21 23:12:02 +0800] [18487] [INFO] Starting worker [18487]通过浏览器访问:http://localhost:8000/ 请求堵塞针对上面的例子,假设test() 视图函数的处理需要5秒钟,那么请求就堵塞了。 from time import sleep app = Sanic(name="pyapp") @app.route('/')async def test(request): sleep(5) return json({'hello': 'world'}) ……重启服务,通过浏览器发送请求,我们发现请求耗时5秒,这显然对用户就不能忍受的。 异步非堵塞所以,我们要实现异步调用,修改后的完整代码如下: import asynciofrom sanic import Sanicfrom sanic.response import jsonfrom sanic.exceptions import NotFoundfrom time import sleep, ctime app = Sanic(name="pyapp") async def task_sleep(): print('sleep before', ctime()) await asyncio.sleep(5) print('sleep after', ctime()) @app.route('/')async def test(request): myLoop = request.app.loop myLoop.create_task(task_sleep()) return json({'hello': 'world'}) if name == '__main__': app.error_handler.add( NotFound, lambda r, e: sanic.response.empty(status=404) app.run(host='0.0.0.0', port=8000) 关于python异步的使用参考上一篇文章,重新启动服务。这次前端就不在堵塞了。 如果看 sanic 的运行日志: sleep before Tue Apr 21 23:43:14 2020sleep after Tue Apr 21 23:43:19 2020他仍然在执行,但不会堵塞test()视图函数的响应。 原文地址https://www.cnblogs.com/fnng/p/12783542.html

java并发编程

java并发编程 常用的java并发编程技术。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/master/spring-currency 一、概述传统的java并发能力依靠的是多线程,相比于现代的方法是Reactive编程,本文介绍多线程的实现,Reactive编程方法的介绍可参见Reactive编程。 多线程并发编程有2个核心概念,原子性和可见性。原子性的介绍随处可见,简单来说就是一组操作要么全部成功,要么全部失败,不存在中间状态。 可见性是指一个线程中数据的变化是否能被其它线程感知。 多线程编程中要一直注意的一个问题点就是check-then-act的处理,我们的程序中存着大量的 条件判断-&gt;执行 的处理,这种简单的处理在单线程中不会存在什么问题,但是在多线程环境中却是极易出错。需要综合考虑原子性和可见性。“竞态条件”表述的就是这个问题。 本文主要介绍的内容:竞态条件、java内存模型(happens-before)、synchronized、原子类、锁、ThreadLocal变量、CountDownLatch、CompletableFuture。 二、竞态条件当多个线程对共享资源进行处理的时候,可能由于不同的执行顺序导致产生不同的结果。比较典型的是check-then-act操作,如以下代码: class Race { private Long value; Long get(){ if( value == null ){ value = initialize(); return value; }}当这个类的同一个对象在多个线程中执行get方法时,由于get方法不是原子操作,initialize方法可能会执行多次。解决这个问题可以通过将get方法改为synchronized方法或者是将value改为原子类。 再来看另外一种竞态条件。 class Waiter implements Runnable { private boolean shouldFinish; void finish() { shouldFinish = true; public void run() { long iteration = 0; while (!shouldFinish) { iteration++; System.out.println("Finished after: " + iteration); }public class DataRace { public static void main(String[] args) throws InterruptedException { Waiter waiter = new Waiter(); Thread waiterThread = new Thread(waiter); waiterThread.start(); // 在另一个的线程中执行waiter的run方法,该方法通过判断shouldFinish变量的值确定是否退出循环 waiter.finish(); // 在主线程中修改shouldFinish变量的值 waiterThread.join(); }正常情况下在执行完waiter的finish方法后,run方法中的循环会退出,但是也有可能run方法会进入死循环。我们可以通过延迟waiter.finish()的执行来模拟这种情况。将main方法做一下修改: public class DataRace { public static void main(String[] args) throws InterruptedException { Waiter waiter = new Waiter(); Thread waiterThread = new Thread(waiter); waiterThread.start(); Thread.sleep(10L); // 延迟10毫秒后再调用finish方法,会发现程序会一直运行不退出,在run方法中shouldFinish一直是false waiter.finish(); waiterThread.join(); }再次执行这个程序,会发现run方法进入了死循环,即使waiter.finish()已经将shouldFinish设置成true,循环仍然没有退出。产生这个问题的原因就是在另一个的线程中读到的shouldFinish变量的值是脏数据。 可以通过将 shouldFinish 变量声明为 volatile 来解决这个问题。 这种现象是源于java内存模型的happens-before规则,一个线程对变量的写入操作的结果只有符合happens-before规则情况下才会被其它线程读取到。 synchronized和volatile结构,以及Thread.start()和Thread.join()方法均可构成happens-before关系。该规则的描述如下(原文): 程序的顺序性规则:一个线程中,按照程序的顺序,前面的操作happens-before后续的任何操作。volatile规则:对一个volatile变量的写操作,happens-before后续对这个变量的读操作。锁规则:对一个锁的解锁操作,happens-before后续对这个锁的加锁操作。线程start()规则:主线程A启动线程B,线程B中可以看到主线程启动B之前的操作。也就是start() happens before 线程B中的操作。线程join()规则:主线程A等待子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作。也就是说,子线程B中的任意操作,happens-before join()的返回。传递性规则:如果A happens-before B,B happens-before C,那么A happens-before C。所以将 shouldFinish 变量声明为 volatile后,符合规则3,执行finish方法后对shouldFinish的修改会被读线程读取到修改后的结果。如果没有加volatile关键字,就没有符合happens-before规则。 三、synchronizedsynchronized 提供了一种悲观锁机制,synchronized声明的代码块具有排他性,同一时间只有一个线程能够获得锁,通过这种方式确保原子性和可见性。synchronized可以用在方法上,也可以用在一段代码块上。当synchronized可以用在static方法上时,用的是类锁,否则是对象锁。 对代码块枷锁: class SynchronizedBlock { private int counter0; void increment() { synchronized (this) { counter0++; }对方法枷锁: class SynchronizedMethod { private int counter0; synchronized void increment() { counter0++; }四、ThreadLocal虽然通过synchronized可以实现原子性,但是由于使用的是悲观锁机制,对性能会有影响。如果多个线程之间的变量不需要共享,可以采用ThreadLocal变量,避免多个线程同时修改同一个变量导致出现并发问题。 class ThreadLocalDemo { private final ThreadLocal&lt;Transaction&gt; currentTransaction = ThreadLocal.withInitial(NullTransaction::new); Transaction currentTransaction() { Transaction current = currentTransaction.get(); if (current.isNull()) { current = new TransactionImpl(); currentTransaction.set(current); return current; interface Transaction { boolean isNull(); class NullTransaction implements Transaction { public boolean isNull() { return true; class TransactionImpl implements Transaction { public boolean isNull() { return false; }五、Atomics另外一种简化并发编程的方式是采用原子数据结构,这种数据结构本身保证了原子性和可见性,可以方便的使用,能够避免多线程环境下check-then-act的问题。 public class Atomic { public static void main(String[] args) { AtomicRun atomicRun = new AtomicRun(); Thread waiterThread1 = new Thread(atomicRun); Thread waiterThread2 = new Thread(atomicRun); waiterThread1.start(); waiterThread2.start(); class AtomicRun implements Runnable { private final AtomicBoolean shouldFinish = new AtomicBoolean(false); public void run() { if (shouldFinish.compareAndSet(false, true)) { System.out.println("initialized only once"); }由于shouldFinish是一个原子对象,shouldFinish.compareAndSet是一个原子操作,因此不会出现读取到脏数据的问题。 六、Locksjava.util.concurrent.locks包提供了与synchronized相同的功能,在此基础上又进行了扩展,例如可以获取锁的状态,可以中断锁。对于读多写少的情况,还可以通过ReadWriteLock来提升性能。 class LockDemo { private final Lock lock = new ReentrantLock(); private int counter0; public static void main(String[] args) { LockDemo lockDemo = new LockDemo(); lockDemo.increment(); System.out.println("count is: " + lockDemo.getCounter0()); public int getCounter0() { return counter0; void increment() { lock.lock(); try { counter0++; } finally { lock.unlock(); class ReadWriteLockDemo { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private int counter1; void increment() { lock.writeLock().lock(); try { counter1++; } finally { lock.writeLock().unlock(); int current() { lock.readLock().lock(); try { return counter1; } finally { lock.readLock().unlock(); }在使用locks时,要注意一定要在finally方法中执行unlock操作,因为程序出现异常后,不会自动释放锁,如果不在finally方法中执行unlock,会导致程序进入死锁状态。 七、CountDownLatchCountDownLatch一般用于同步多个线程的执行进度,例如有一个线程需要等其它三个线程执行完成后,再继续往下执行,可以用CountDownLatch来处理。 CountDownLatch类似于一个计数器,当一个线程调用CountDownLatch的await方法时会进入阻塞状态,其它线程调用countDown方法对计数器减一,当计数器减为0时,被await方法阻塞的操作才会解除阻塞状态继续执行。 public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(2); CountDownLatch latch = new CountDownLatch(1); Receiver receiver = new Receiver(latch); executorService.submit(receiver); latch.await(); System.out.println("latch done"); executorService.shutdown(); class Receiver implements Runnable { private CountDownLatch latch; public Receiver(CountDownLatch latch) { this.latch = latch; public void run() { latch.countDown(); }八、CompletableFutureCompletableFuture是一种java8提供的常用的多线程并发编程方法,虽然parallelStream也提供了多线程并发能力,但是在选择上要遵循一个原则:有IO操作的用CompletableFuture,没有IO操作纯计算的用parallelStream。 原因在于parallelStream使用的是jvm的默认ForkJoinPool线程池,该线程池一般只会分配很少的线程数(默认是CPU的核数),不能指定其它线程池。当有IO操作或者类似的延迟较高的操作时,很容易把线程池占满。而CompletableFuture允许指定线程池,可以为不同的处理指定不同的线程池,能够分业务进行线程池的隔离。 首先我们模拟一个延迟IO方法,用于后续的演示: public static Long getPrice(String prod) { delay(); //模拟服务响应的延迟 Long price = ThreadLocalRandom.current().nextLong(0, 1000); System.out.println("Executing in " + Thread.currentThread().getName() + ", get price for " + prod + " is " + price); return price; private static void delay() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); supplyAsync:该方法用于创建一个异步任务ExecutorService executor = Executors.newFixedThreadPool(10); CompletableFuture future = CompletableFuture.supplyAsync(() -&gt; getPrice("accept"), executor);supplyAsync是一个工厂方法,该方法会返回一个CompletableFuture对象,入参是 Supplier 或者 Runnable 的实现类,可以用Lambda表达式来表示。同时指定了executor作为执行用的线程池。 thenAcceptAsync:该方法接收CompletableFuture的执行结果,将结果作为输入执行指定的方法future.thenAccept(p -&gt; { System.out.println("Executing in " + Thread.currentThread().getName() + ", async price is: " + p); }, executor); 将第一步中的返回结果(getPrice的返回值)作为输入执行操作。 thenApply:该方法接收CompletableFuture的执行结果进行计算,返回一个新的CompletableFuture,类似于stream的map操作。CompletableFuture result = future.thenApply(p -&gt; p + "1");该步操作将第一步中的CompletableFuture 转换为了 CompletableFuture。 thenCompose:用于组合2个CompletableFuture,第一个的计算结果作为第二个的输入。CompletableFuture future1 = CompletableFuture .supplyAsync(() -&gt; getPrice("compose")); CompletableFuture result = future1.thenCompose( i -&gt; CompletableFuture.supplyAsync(() -&gt; { Thread.sleep(2000); return i + "World"; );thenCombine: 将两个CompletableFuture的计算结果做进一步的计算CompletableFuture future1 = CompletableFuture.supplyAsync(() -&gt; getPrice("combine1"));CompletableFuture future2 = CompletableFuture.supplyAsync(() -&gt; getPrice("combine2"));CompletableFuture result = future1.thenCombine(future2, (f1, f2) -&gt; f1 + f2);这段代码会在future1和future2都计算完成后,把两个future的计算结果进行相加,返回新的CompletableFuture。 exceptionally: 异常处理exceptionally是CompletableFuture最简便的一种异常处理方法。该方法会在异常发生后返回一个默认值。 CompletableFuture future1 = CompletableFuture.supplyAsync(() -&gt; getPrice("exception1")); CompletableFuture future2 = CompletableFuture .supplyAsync(() -&gt; (1L / 0) ) //模拟抛出一个异常 // 出现异常时返回默认值,如果此处没有exceptionally处理,异常会在后续的join中抛出 .exceptionally((ex) -&gt; { System.out.println("Executing in " + Thread.currentThread().getName() + ", get excetion " + ex); return 0L; CompletableFuture result = future1.thenCombine(future2, (f1, f2) -&gt; f1 + f2); try { System.out.println("Executing in " + Thread.currentThread().getName() + " ,combine price is: " + result.join()); } catch (CompletionException ex) { System.out.println("Executing in " + Thread.currentThread().getName() + " ,combine price error: " + ex); }并行执行CompletableFuture假设我们有一个数组,数组中的每一项都需要调用getPrice方法获取价格,可以采用stream和CompletableFuture组合使用的方式。 List prices = Stream.of("1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12") .map(p -&gt; CompletableFuture.supplyAsync(() -&gt; getPrice("exception1"), executor)) //通过stream的map操作,为数组中的每一个元素都启动一个CompletableFuture .collect(Collectors.toList()) .stream() .map(CompletableFuture::join) //等待所有的CompletableFuture都完成计算 .collect(Collectors.toList()); 注意这儿要有两段collect处理,不能简化为以下写法: ist prices2 = Stream.of("1", "2", "3") .map(p -&gt; CompletableFuture.supplyAsync(() -&gt; getPrice("exception1"))) .map(CompletableFuture::join) .collect(Collectors.toList()); 这样写看上去更简洁,但是存在严重的问题。因为对于每一个元素,在第一个map生成CompletableFuture后,会立即执行join阻塞操作,相当于变成了串行。 原文地址https://my.oschina.net/hiease/blog/4227185

ASP.NET Core技术研究-全面认识Web服务器Kestrel

ASP.NET Core技术研究-全面认识Web服务器Kestrel 因为IIS不支持跨平台的原因,我们在升级到ASP.NET Core后,会接触到一个新的Web服务器Kestrel。相信大家刚接触这个Kestrel时,会有各种各样的疑问。 今天我们全面认识一下ASP.NET Core的默认Web服务器Kestrel。 一、初识Kestrel 首先,Kestrel是一个跨平台的Web服务器,支持运行在Windows、macOS、Linux等操作系统中。Kestrel支持一下使用场景: HTTPSOpaque upgrade used to enable WebSockets(启用WebSocket情况下的不透明升级)Unix sockets for high performance behind Nginx(Nginx高性能模式下的Unix套接字)HTTP2(不支持macOS)Kestrel支持运行在所有.NET 支持的平台和版本之上。 二、Kestrel主要应用场景   Kestrel主要有两种使用模式:   1. Kestrel直接作为Web服务器,直接接收并处理各类Http请求:      2. 与各类反向代理服务器(例如Nginx、Apache、IIS)配合使用,反向代理服务器接收Http请求,将这些请求转发到Kestrel Web服务器          使用反向代理服务器的好处有哪些呢?   对外暴露有限的HTTP服务   更加安全,反向代理服务器做了一层过滤、防护和转发   通过反向代理服务器实现负载均衡和动态请求分发路由   减少域名使用,降低WAF防火墙防护成本   安全通信 (HTTPS) 配置,HTTPS转HTTP,仅反向代理服务器需要 X.509 证书,并且该服务器可使用普通 HTTP 协议与内部网络的应用服务器通信。 三、Kestrel支持特性之-HTTP/2 Kestrel在以下操作系统和.NET Core版本下支持HTTP/2   操作系统: Windows Server 2016/Windows 10 或更高版本具有 OpenSSL 1.0.2 或更高版本的 Linux(例如,Ubuntu 16.04 或更高版本)macOS 的未来版本将支持 HTTP/2macOS 的未来版本将支持 †HTTP/2。 ‡Kestrel 在 Windows Server 2012 R2 和 Windows 8.1 上对 HTTP/2 的支持有限。 目标框架:.NET Core 2.2 或更高版本 关于HTTP/2 可以参考一下超链接:https://http2.github.io/ 关于HTTP/2和HTTP/1.1的全方位对比,可以参考这个超链接:https://cheapsslsecurity.com/p/http2-vs-http1/  四、在ASP.NET Core中使用Kestrel 在ASP.NET Core的框架Microsoft.AspNetCore.App内置了package:Microsoft.AspNetCore.Server.Kestrel ,即原生对Kestrel的支持: 大家可以找到ASP.NET Core 3.1的本地目录:C:Program FilesdotnetpacksMicrosoft.AspNetCore.App.Ref3.1.0refnetcoreapp3.1 中找到Kestrel相关的dll: 当我们新建一个ASP.NET Core Project,在Program.cs类中有以下代码, 1234567891011121314public class Program{        public static void Main(string[] args)        {            CreateHostBuilder(args).Build().Run();        }         public static IHostBuilder CreateHostBuilder(string[] args) =&gt;            Host.CreateDefaultBuilder(args)                .ConfigureWebHostDefaults(webBuilder =&gt;                {                    webBuilder.UseStartup();                });}  我们通过查看ConfigureWebDefaults的实现源码可以发现,在其内部调用了UseKestrel()方法,即ASP.NET Core默认使用Kestrel Web服务器! 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051internal static void ConfigureWebDefaults(IWebHostBuilder builder)       {           builder.ConfigureAppConfiguration((ctx, cb) =&gt;           {               if (ctx.HostingEnvironment.IsDevelopment())               {                   StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);               }           });           builder.UseKestrel((builderContext, options) =&gt;           {               options.Configure(builderContext.Configuration.GetSection("Kestrel"));           })           .ConfigureServices((hostingContext, services) =&gt;           {               // Fallback               services.PostConfigure(options =&gt;               {                   if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)                   {                       // "AllowedHosts": "localhost;127.0.0.1;[::1]"                       var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);                       // Fall back to "*" to disable.                       options.AllowedHosts = (hosts?.Length &gt; 0 ? hosts : new[] { "*" });                   }               });               // Change notification               services.AddSingleton&gt;(                           new ConfigurationChangeTokenSource(hostingContext.Configuration));                 services.AddTransient();                 if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))               {                   services.Configure(options =&gt;                   {                       options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;                       // Only loopback proxies are allowed by default. Clear that restriction because forwarders are                       // being enabled by explicit configuration.                       options.KnownNetworks.Clear();                       options.KnownProxies.Clear();                   });                     services.AddTransient();               }                 services.AddRouting();           })           .UseIIS()           .UseIISIntegration();       }以上详细的代码可以参考,上一篇博文:.NET Core技术研究-主机Host 五、Kestrel的配置选项 我们可以使用 webBuilder.ConfigureKestrel设置Kestrel的一些选项: 接下来,我们看一下Kestrel Web服务器提供了哪些选项设置:   1. KeepAliveTimeout:保持活动会话超时时间 默认2分钟,可以用以下代码进行设置: 1serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);  2. 客户端最大连接数: MaxConcurrentConnections、 MaxConcurrentUpgradedConnections 默认情况下,最大连接数不受限制; 可以通过 MaxConcurrentConnections,设置整个应用设置并发打开的最大 TCP 连接数。 对于已从 HTTP 或 HTTPS 升级到另一个协议(例如,Websocket 请求)的连接,有一个单独的限制MaxConcurrentUpgradedConnections。 连接升级后,不会计入 MaxConcurrentConnections 限制。 可以用以下代码进行设置: 12serverOptions.Limits.MaxConcurrentConnections = 100;serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;3. 请求正文最大大小: MaxRequestBodySize 默认的请求正文最大大小为 30,000,000 字节,大约 28.6 MB 1serverOptions.Limits.MaxRequestBodySize = 10 * 1024;在 ASP.NET Core MVC 应用中替代限制的推荐方法是在操作方法上使用 RequestSizeLimitAttribute 属性: 12[RequestSizeLimit(100000000)]public IActionResult MyActionMethod()     4. 请求正文最小数据速率 MinRequestBodyDataRate MinResponseDataRate Kestrel 每秒检查一次数据是否以指定的速率(字节/秒)传入。 如果速率低于最小值,则连接超时。 宽限期是 Kestrel 提供给客户端用于将其发送速率提升到最小值的时间量;在此期间不会检查速率。 宽限期可以尽可能地避免最初由于 TCP 慢启动而以较慢速率发送数据的连接中断。 默认的最小速率为 240 字节/秒,包含 5 秒的宽限期。 最小速率也适用于HttpResponse响应。 除了属性和接口名称中具有 RequestBody 或 Response 以外,用于设置请求限制和响应限制的代码相同。 12serverOptions.Limits.MinRequestBodyDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));serverOptions.Limits.MinResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));  5. 请求Header超时 RequestHeadersTimeout 获取或设置服务器接收请求标头所花费的最大时间量。 默认值为 30 秒。 1serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);   6. 每个连接的最大的请求流的数量 MaxStreamsPerConnection Http2.MaxStreamsPerConnection 限制每个 HTTP/2 连接的并发请求流的数量。 拒绝过多的流。 1serverOptions.Limits.Http2.MaxStreamsPerConnection = 100; 7. 标题表大小 HPACK 解码器解压缩 HTTP/2 连接的 HTTP 标头。 Http2.HeaderTableSize 限制 HPACK 解码器使用的标头压缩表的大小。 该值以八位字节提供,且必须大于零 (0)。 1serverOptions.Limits.Http2.HeaderTableSize = 4096; 8. 最大帧大小 Http2.MaxFrameSize Http2.MaxFrameSize 表示服务器接收或发送的 HTTP/2 连接帧有效负载的最大允许大小。 该值以八位字节提供,必须介于 2^14 (16,384) 和 2^24-1 (16,777,215) 之间。   1serverOptions.Limits.Http2.MaxFrameSize = 16384; 最大请求头大小 Http2.MaxRequestHeaderFieldSize   Http2.MaxRequestHeaderFieldSize 表示请求标头值的允许的最大大小(用八进制表示)。 此限制适用于名称和值的压缩和未压缩表示形式。 该值必须大于零 (0)。      1serverOptions.Limits.Http2.MaxRequestHeaderFieldSize = 8192; 初始连接窗口大小  Http2.InitialConnectionWindowSize  Http2.InitialConnectionWindowSize 表示服务器一次性缓存的最大请求主体数据大小(每次连接时在所有请求(流)中汇总,以字节为单位)。 请求也受 Http2.InitialStreamWindowSize 限制。 该值必须大于或等于 65,535,并小于 2^31 (2,147,483,648)。        默认值为 128 KB (131,072) 1serverOptions.Limits.Http2.InitialConnectionWindowSize = 131072; 11. 初始流窗口大小 Http2.InitialStreamWindowSize Http2.InitialStreamWindowSize 表示服务器针对每个请求(流)的一次性缓存的最大请求主体数据大小(以字节为单位)。 请求也受 Http2.InitialConnectionWindowSize 限制。 该值必须大于或等于 65,535,并小于 2^31 (2,147,483,648)。     默认值为 96 KB (98,304)     1serverOptions.Limits.Http2.InitialStreamWindowSize = 98304; 12. 同步IO  AllowSynchronousIO  AllowSynchronousIO 控制是否允许对请求和响应使用同步 IO。 默认值为 false。这个设置需要注意一下: 大量的阻止同步 IO 操作可能会导致线程池资源不足,进而导致应用无响应。 仅在使用不支持异步 IO 的库时,才启用 AllowSynchronousIO。 1serverOptions.AllowSynchronousIO = true; 以上是ASP.NET Core Web服务器Kestrel的一些研究和梳理,分享给大家。 原文地址https://www.cnblogs.com/tianqing/p/12764404.html

Spring Cloud 系列之 Gateway 服务网关(四) 本篇文章为系列文章,未读第一集的同学请猛戳这里: Spring Cloud 系列之 Gateway 服务网关(一)Spring Cloud 系列之 Gateway 服务网关(二)Spring Cloud 系列之 Gateway 服务网关(三)本篇文章讲解 Gateway 网关如何实现限流、整合 Sentinel 实现限流以及高可用网关环境搭建。 顾名思义,限流就是限制流量,就像你宽带包有 1 个 G 的流量,用完了就没了。通过限流,我们可以很好地控制系统的 QPS,从而达到保护系统的目的。 为什么需要限流 比如 Web 服务、对外 API,这种类型的服务有以下几种可能导致机器被拖垮: 用户增长过快(好事)因为某个热点事件(微博热搜)竞争对象爬虫恶意的请求这些情况都是无法预知的,不知道什么时候会有 10 倍甚至 20 倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的。 从上图可以看出,对内而言:上游的 A、B 服务直接依赖了下游的基础服务 C,对于 A,B 服务都依赖的基础服务 C 这种场景,服务 A 和 B 其实处于某种竞争关系,如果服务 A 的并发阈值设置过大,当流量高峰期来临,有可能直接拖垮基础服务 C 并影响服务 B,即雪崩效应。 点击链接观看:限流算法视频(获取更多请关注公众号「哈喽沃德先生」) 常见的限流算法有: 计数器算法漏桶(Leaky Bucket)算法令牌桶(Token Bucket)算法 计数器算法 计数器算法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于 A 接口来说,我们 1 分钟的访问次数不能超过 100 个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器 counter,每当一个请求过来的时候,counter 就加 1,如果 counter 的值大于 100 并且该请求与第一个请求的间隔时间还在 1 分钟之内,触发限流;如果该请求与第一个请求的间隔时间大于 1 分钟,重置 counter 重新计数,具体算法的示意图如下: 这个算法虽然简单,但是有一个十分致命的问题,那就是临界问题,我们看下图: 从上图中我们可以看到,假设有一个恶意用户,他在 0:59 时,瞬间发送了 100 个请求,并且 1:00 又瞬间发送了 100 个请求,那么其实这个用户在 1 秒里面,瞬间发送了 200 个请求。我们刚才规定的是 1 分钟最多 100 个请求,也就是每秒钟最多 1.7 个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。 还有资料浪费的问题存在,我们的预期想法是希望 100 个请求可以均匀分散在这一分钟内,假设 30s 以内我们就请求上限了,那么剩余的半分钟服务器就会处于闲置状态,比如下图: 漏桶算法其实也很简单,可以粗略的认为就是注水漏水的过程,往桶中以任意速率流入水,以一定速率流出水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。 漏桶算法是使用队列机制实现的。 漏桶算法主要用途在于保护它人(服务),假设入水量很大,而出水量较慢,则会造成网关的资源堆积可能导致网关瘫痪。而目标服务可能是可以处理大量请求的,但是漏桶算法出水量缓慢反而造成服务那边的资源浪费。 漏桶算法无法应对突发调用。不管上面流量多大,下面流出的速度始终保持不变。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就会丢弃。 令牌桶算法 令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。 场景大概是这样的:桶中一直有大量的可用令牌,这时进来的请求可以直接拿到令牌执行,比如设置 QPS 为 100/s,那么限流器初始化完成一秒后,桶中就已经有 100 个令牌了,等服务启动完成对外提供服务时,该限流器可以抵挡瞬时的 100 个请求。当桶中没有令牌时,请求会进行等待,最后相当于以一定的速率执行。 Spring Cloud Gateway 内部使用的就是该算法,大概描述如下: 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;根据限流大小,设置按照一定的速率往桶里添加令牌;桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;请求到达后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。 漏桶算法主要用途在于保护它人,而令牌桶算法主要目的在于保护自己,将请求压力交由目标服务处理。假设突然进来很多请求,只要拿到令牌这些请求会瞬时被处理调用目标服务。 Gateway 限流 点击链接观看:Gateway 服务限流视频(获取更多请关注公众号「哈喽沃德先生」) Spring Cloud Gateway 官方提供了 RequestRateLimiterGatewayFilterFactory 过滤器工厂,使用 Redis 和 Lua 脚本实现了令牌桶的方式。 官网文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#the-redis-ratelimiter 具体实现逻辑在 RequestRateLimiterGatewayFilterFactory 类中,Lua 脚本在如下图所示的源码文件夹中: org.springframework.bootspring-boot-starter-data-redis-reactiveorg.apache.commonscommons-pool2 URI 限流 配置限流过滤器和限流过滤器引用的 bean 对象。 spring: application: name: gateway-server # 应用名称 cloud: gateway: # 路由规则 routes: - id: product-service # 路由 ID,唯一 uri: lb://product-service # lb:// 根据服务名称从注册中心获取服务请求地址 predicates: # 断言(判断条件) # 匹配对应 URI 的请求,将匹配到的请求追加在目标 URI 之后 - Path=/product/** filters: # 网关过滤器 # 限流过滤器 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率 redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量 key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表达式按名称引用 bean # redis 缓存 redis: timeout: 10000 # 连接超时时间 host: 192.168.10.101 # Redis服务器地址 port: 6379 # Redis服务器端口 password: root # Redis服务器密码 database: 0 # 选择哪个库,默认0库 lettuce: pool: max-active: 1024 # 最大连接数,默认 8 max-wait: 10000 # 最大连接阻塞等待时间,单位毫秒,默认 -1 max-idle: 200 # 最大空闲连接,默认 8 min-idle: 5 # 最小空闲连接,默认 0 编写限流规则配置类。 package com.example.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; / 限流规则配置类 / @Configuration public class KeyResolverConfiguration { / 限流规则 @return / @Bean public KeyResolver pathKeyResolver() { / return new KeyResolver() { @Override public Mono resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getPath().toString()); } }; / // JDK 1.8 return exchange -&gt; Mono.just(exchange.getRequest().getURI().getPath()); } } 多次访问:http://localhost:9000/product/1 结果如下: Redis 结果如下: 配置限流过滤器和限流过滤器引用的 bean 对象。 spring: application: name: gateway-server # 应用名称 cloud: gateway: # 路由规则 routes: - id: product-service # 路由 ID,唯一 uri: lb://product-service # lb:// 根据服务名称从注册中心获取服务请求地址 predicates: # 断言(判断条件) # 匹配对应 URI 的请求,将匹配到的请求追加在目标 URI 之后 - Path=/product/** filters: # 网关过滤器 # 限流过滤器 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率 redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量 key-resolver: "#{@parameterKeyResolver}" # 使用 SpEL 表达式按名称引用 bean # redis 缓存 redis: timeout: 10000 # 连接超时时间 host: 192.168.10.101 # Redis服务器地址 port: 6379 # Redis服务器端口 password: root # Redis服务器密码 database: 0 # 选择哪个库,默认0库 lettuce: pool: max-active: 1024 # 最大连接数,默认 8 max-wait: 10000 # 最大连接阻塞等待时间,单位毫秒,默认 -1 max-idle: 200 # 最大空闲连接,默认 8 min-idle: 5 # 最小空闲连接,默认 0 编写限流规则配置类。 package com.example.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; / 限流规则配置类 / @Configuration public class KeyResolverConfiguration { / 根据参数限流 @return / @Bean public KeyResolver parameterKeyResolver() { return exchange -&gt; Mono.just(exchange.getRequest().getQueryParams().getFirst("userId")); } } 多次访问:http://localhost:9000/product/1?userId=123 结果如下: Redis 结果如下: IP 限流 配置限流过滤器和限流过滤器引用的 bean 对象。 spring: application: name: gateway-server # 应用名称 cloud: gateway: # 路由规则 routes: - id: product-service # 路由 ID,唯一 uri: lb://product-service # lb:// 根据服务名称从注册中心获取服务请求地址 predicates: # 断言(判断条件) # 匹配对应 URI 的请求,将匹配到的请求追加在目标 URI 之后 - Path=/product/** filters: # 网关过滤器 # 限流过滤器 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率 redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量 key-resolver: "#{@ipKeyResolver}" # 使用 SpEL 表达式按名称引用 bean # redis 缓存 redis: timeout: 10000 # 连接超时时间 host: 192.168.10.101 # Redis服务器地址 port: 6379 # Redis服务器端口 password: root # Redis服务器密码 database: 0 # 选择哪个库,默认0库 lettuce: pool: max-active: 1024 # 最大连接数,默认 8 max-wait: 10000 # 最大连接阻塞等待时间,单位毫秒,默认 -1 max-idle: 200 # 最大空闲连接,默认 8 min-idle: 5 # 最小空闲连接,默认 0 编写限流规则配置类。 package com.example.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; / 限流规则配置类 / @Configuration public class KeyResolverConfiguration { / 根据 IP 限流 @return / @Bean public KeyResolver ipKeyResolver() { return exchange -&gt; Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } } 多次访问:http://localhost:9000/product/1 结果如下: Redis 结果如下: Sentinel 限流 点击链接观看:Sentinel 服务限流视频(获取更多请关注公众号「哈喽沃德先生」) Sentinel 支持对 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 进行限流。 官网文档: https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinelhttps://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81#spring-cloud-gateway 创建 gateway-server-sentinel 项目。 单独使用添加 sentinel gateway adapter 依赖即可。 若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway 依赖来让 spring-cloud-alibaba-sentinel-gateway 模块里的 Spring Cloud Gateway 自动化配置类生效。 同时请将 spring.cloud.sentinel.filter.enabled 配置项置为 false(若在网关流控控制台上看到了 URL 资源,就是此配置项没有置为 false)。 &lt;?xml version="1.0" encoding="UTF-8"?&gt; http://maven.apache.org/xsd/maven-4.0.0.xsd"&gt; 4.0.0com.examplegateway-server-sentinel1.0-SNAPSHOTcom.examplegateway-demo1.0-SNAPSHOTorg.springframework.cloudspring-cloud-starter-gatewayorg.springframework.cloudspring-cloud-starter-netflix-eureka-clientcom.alibaba.cspsentinel-spring-cloud-gateway-adapter com.alibaba.cloud spring-cloud-starter-alibaba-sentinel com.alibaba.cloud spring-cloud-alibaba-sentinel-gateway --> server: port: 9001 # 端口 spring: application: name: gateway-server-sentinel # 应用名称 cloud: sentinel: filter: enabled: false gateway: discovery: locator: # 是否与服务发现组件进行结合,通过 serviceId 转发到具体服务实例。 enabled: true # 是否开启基于服务发现的路由规则 lower-case-service-id: true # 是否将服务名称转小写 # 路由规则 routes: - id: order-service # 路由 ID,唯一 uri: lb://order-service # 目标 URI,lb:// 根据服务名称从注册中心获取服务请求地址 predicates: # 断言(判断条件) # 匹配对应 URI 的请求,将匹配到的请求追加在目标 URI 之后 - Path=/order/** # 配置 Eureka Server 注册中心 eureka: instance: prefer-ip-address: true # 是否使用 ip 地址注册 instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port client: service-url: # 设置服务注册中心地址 defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/ 限流规则配置类 使用时只需注入对应的 SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可。 GatewayConfiguration.java package com.example.config; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.result.view.ViewResolver; import javax.annotation.PostConstruct; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; / 限流规则配置类 / @Configuration public class GatewayConfiguration { private final List viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; / 构造器 @param viewResolversProvider @param serverCodecConfigurer / public GatewayConfiguration(ObjectProvider&gt; viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } / 限流异常处理器 @return / @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } / 限流过滤器 @return / @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } / Spring 容器初始化的时候执行该方法 / @PostConstruct public void doInit() { // 加载网关限流规则 initGatewayRules(); } / 网关限流规则 / private void initGatewayRules() { Set rules = new HashSet&lt;&gt;(); / resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称 count:限流阈值 intervalSec:统计时间窗口,单位是秒,默认是 1 秒 */ rules.add(new GatewayFlowRule("order-service") .setCount(3) // 限流阈值 .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒 // 加载网关限流规则 GatewayRuleManager.loadRules(rules); } } GatewayServerSentinelApplication.java package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解 //@EnableEurekaClient @SpringBootApplication public class GatewayServerSentinelApplication { public static void main(String[] args) { SpringApplication.run(GatewayServerSentinelApplication.class, args); } } 多次访问:http://localhost:9001/order/1 结果如下: 接口 BlockRequestHandler 的默认实现为 DefaultBlockRequestHandler,当触发限流时会返回默认的错误信息:Blocked by Sentinel: FlowException。我们可以通过 GatewayCallbackManager 定制异常提示信息。 自定义异常提示 GatewayCallbackManager 的 setBlockHandler 注册函数用于实现自定义的逻辑,处理被限流的请求。 package com.example.config; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import java.util.; / 限流规则配置类 / @Configuration public class GatewayConfiguration { private final List viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; / 构造器 @param viewResolversProvider @param serverCodecConfigurer / public GatewayConfiguration(ObjectProvider&gt; viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } / 限流异常处理器 @return / @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } / 限流过滤器 @return / @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } / Spring 容器初始化的时候执行该方法 / @PostConstruct public void doInit() { // 加载网关限流规则 initGatewayRules(); // 加载自定义限流异常处理器 initBlockHandler(); } / 网关限流规则 / private void initGatewayRules() { Set rules = new HashSet&lt;&gt;(); / resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称 count:限流阈值 intervalSec:统计时间窗口,单位是秒,默认是 1 秒 / rules.add(new GatewayFlowRule("order-service") .setCount(3) // 限流阈值 .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒 // 加载网关限流规则 GatewayRuleManager.loadRules(rules); } /* 自定义限流异常处理器 */ private void initBlockHandler() { BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @Override public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map result = new HashMap&lt;&gt;(); result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value())); result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase()); result.put("route", "order-service"); return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(result)); } }; // 加载自定义限流异常处理器 GatewayCallbackManager.setBlockHandler(blockRequestHandler); } } 多次访问:http://localhost:9001/order/1 结果如下: package com.example.config; import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import java.util.; / 限流规则配置类 / @Configuration public class GatewayConfiguration { private final List viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; / 构造器 @param viewResolversProvider @param serverCodecConfigurer / public GatewayConfiguration(ObjectProvider&gt; viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } / 限流异常处理器 @return / @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } / 限流过滤器 @return / @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } / Spring 容器初始化的时候执行该方法 / @PostConstruct public void doInit() { // 加载网关限流规则 initGatewayRules(); // 加载自定义限流异常处理器 initBlockHandler(); } / 网关限流规则 / private void initGatewayRules() { Set rules = new HashSet&lt;&gt;(); / resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称 count:限流阈值 intervalSec:统计时间窗口,单位是秒,默认是 1 秒 / // rules.add(new GatewayFlowRule("order-service") // .setCount(3) // 限流阈值 // .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒 // --------------------限流分组----------start---------- rules.add(new GatewayFlowRule("product-api") .setCount(3) // 限流阈值 .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒 rules.add(new GatewayFlowRule("order-api") .setCount(5) // 限流阈值 .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒 // --------------------限流分组-----------end----------- // 加载网关限流规则 GatewayRuleManager.loadRules(rules); // 加载限流分组 initCustomizedApis(); } / 自定义限流异常处理器 / private void initBlockHandler() { BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @Override public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map result = new HashMap&lt;&gt;(); result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value())); result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase()); result.put("route", "order-service"); return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(result)); } }; // 加载自定义限流异常处理器 GatewayCallbackManager.setBlockHandler(blockRequestHandler); } / 限流分组 / private void initCustomizedApis() { Set definitions = new HashSet&lt;&gt;(); // product-api 组 ApiDefinition api1 = new ApiDefinition("product-api") .setPredicateItems(new HashSet() {{ // 匹配 /product-service/product 以及其子路径的所有请求 add(new ApiPathPredicateItem().setPattern("/product-service/product/**") .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); // order-api 组 ApiDefinition api2 = new ApiDefinition("order-api") .setPredicateItems(new HashSet() {{ // 只匹配 /order-service/order/index add(new ApiPathPredicateItem().setPattern("/order-service/order/index")); }}); definitions.add(api1); definitions.add(api2); // 加载限流分组 GatewayApiDefinitionManager.loadApiDefinitions(definitions); } } 访问:http://localhost:9001/product-service/product/1 触发限流 访问:http://localhost:9001/order-service/order/index 触发限流 访问:http://localhost:9001/order-service/order/1 不会触发限流 高可用网关 业内通常用多少 9 来衡量网站的可用性,例如 QQ 的可用性是 4 个 9,就是说 QQ 能够保证在一年里,服务在 99.99% 的时间是可用的,只有 0.01% 的时间不可用,大约最多 53 分钟。 对于大多数网站,2 个 9 是基本可用;3 个 9 是叫高可用;4 个 9 是拥有自动恢复能力的高可用。 实现高可用的主要手段是数据的冗余备份和服务的失效转移,这两种手段具体可以怎么做呢,在网关里如何体现?主要有以下几个方向: 集群部署负载均衡健康检查节点自动重启熔断服务降级接口重试 Nginx + 网关集群实现高可用网关 官网:http://nginx.org/en/download.html 下载稳定版。为了方便学习,请下载 Windows 版本。 解压文件后直接运行根路径下的 nginx.exe 文件即可。 Nginx 默认端口为 80,访问:http://localhost:80/ 看到下图说明安装成功。 配置网关集群 进入 Nginx 的 conf 目录,打开 nginx.conf 文件,配置网关集群: http { upstream gateway {server 127.0.0.1:9000;server 127.0.0.1:9001;} server {listen 80;server_name localhost; 代理网关集群,负载均衡调用 location / {proxy_pass http://gateway;} 启动两台网关服务器 http://localhost:9000/,http://localhost:9001/ 和相关服务。 访问:http://localhost/product-service/product/1 实现高可用网关。 一个请求过来,首先经过 Nginx 的一层负载,到达网关,然后由网关负载到真实后端,若后端有问题,网关会进行重试访问,多次访问后仍返回失败,可以通过熔断或服务降级立即返回结果。而且,由于是负载均衡,网关重试时不一定会访问到出错的后端。 至此 Gateway 服务网关所有的知识点就讲解结束了。 原文地址https://www.cnblogs.com/mrhelloworld/p/gateway4.html

意思是java的对象头在对象的不同状态下会有不同的表现形式,主要有三种状态,无锁状态、加锁状态、gc标记状态。 那么我可以理解java当中的取锁其实可以理解是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代码块。 但是java当中的锁有分为很多种,从上图可以看出大体分为偏向锁、轻量锁、重量锁三种锁状态。 这三种锁的效率 完全不同、关于效率的分析会在下文分析,我们只有合理的设计代码,才能合理的利用锁、那么这三种锁的原理是什么? 所以我们需要先研究这个对象头。 java对象的布局以及对象头的布局使用JOL来分析java的对象布局,添加依赖 &lt;groupId&gt;org.openjdk.jol&lt;/groupId&gt; &lt;artifactId&gt;jol-core&lt;/artifactId&gt; &lt;version&gt;0.8&lt;/version&gt; &lt;/dependency&gt; public class B { public class JOLExample1 { static B b = new B(); public static void main(String[] args) { //jvm的信息 out.println(VM.current().details()); out.println(ClassLayout.parseInstance(b).toPrintable()); 分析结果1:整个对象一共16B,其中对象头(Object header)12B,还有4B是对齐的字节(因为在64位虚拟机上对象的大小必 须是8的倍数), 由于这个对象里面没有任何字段,故而对象的实例数据为0B? 1、什么叫做对象的实例数据呢? 2、那么对象头里面的12B到底存的是什么呢? 首先要明白什么对象的实例数据很简单,我们可以在B当中添加一个boolean的字段,大家都知道boolean字段占 1B,然后再看结果 整个对象的大小还是没有改变一共16B,其中对象头(Object header)12B,boolean字段flag(对象的实例数据)占 1B、剩下的3B就是对齐字节。 由此我们可以认为一个对象的布局大体分为三个部分分别是:对象头(Object header)、 对象的实例数据和字节对齐 接下来讨论第二个问题,对象头为什么是12B?这个12B当中分别存储的是什么呢?(不同位数的VM对象头的长度不一 样,这里指的是64bit的vm) 首先引用openjdk文档当中对对象头的解释 上述引用中提到一个java对象头包含了2个word,并且好包含了堆对象的布局、类型、GC状态、同步状态和标识哈 希码,具体怎么包含的呢?又是哪两个word呢? mark word为第一个word根据文档可以知他里面包含了锁的信息,hashcode,gc信息等等,第二个word是什么 呢? klass word为对象头的第二个word主要指向对象的元数据。 假设我们理解一个对象头主要上图两部分组成(数组对象除外,数组对象的对象头还包含一个数组长度), 那么 一个java的对象头多大呢?我们从JVM的源码注释中得知到一个mark word一个是64bit,那么klass的长度是多少呢? 所以我们需要想办法来获得java对象头的详细信息,验证一下他的大小,验证一下里面包含的信息是否正确。 根据上述利用JOL打印的对象头信息可以知道一个对象头是12B,其中8B是mark word 那么剩下的4B就是klass word了,和锁相关的就是mark word了, 那么接下来重点分析mark word里面信息 在无锁的情况下markword当中的前56bit存的是对象的hashcode,那么来验证一下 先上代码:手动计算HashCode public class HashUtil { public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException { // 手动计算HashCode Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); long hashCode = 0; for (long index = 7; index &gt; 0; index--) { // 取Mark Word中的每一个Byte进行计算 hashCode |= (unsafe.getByte(object, index) &amp; 0xFF) &lt;&lt; ((index - 1) * 8); String code = Long.toHexString(hashCode); System.out.println("util-----------0x"+code); public class JOLExample2 { public static void main(String[] args) throws Exception { B b = new B(); out.println("befor hash"); //没有计算HASHCODE之前的对象头 out.println(ClassLayout.parseInstance(b).toPrintable()); //JVM 计算的hashcode out.println("jvm------------0x"+Integer.toHexString(b.hashCode())); HashUtil.countHash(b); //当计算完hashcode之后,我们可以查看对象头的信息变化 out.println("after hash"); out.println(ClassLayout.parseInstance(b).toPrintable()); 分析结果3: 1-----上面没有进行hashcode之前的对象头信息,可以看到的56bit没有值,打印完hashcode之后就有值了,为什 么是1-7B,不是0-6B呢?因为是小端存储。 其中两行是我们通过hashcode方法打印的结果,第一行是我根据1-7B的信息计算出来的 hashcode,所以可以确定java对象头当中的mark work里面的后七个字节存储的是hashcode信息, 那么第一个字节当中的八位分别存的 就是分带年龄、偏向锁信息,和对象状态,这个8bit分别表示的信息如下图(其实上图也有信息),这个图会随着对象状态改变而改变, 下图是无锁状态下 关于对象状态一共分为五种状态,分别是无锁、偏向锁、轻量锁、重量锁、GC标记, 那么2bit,如何能表示五种状 态(2bit最多只能表示4中状态分别是:00,01,10,11), jvm做的比较好的是把偏向锁和无锁状态表示为同一个状态,然 后根据图中偏向锁的标识再去标识是无锁还是偏向锁状态。 什么意思呢?写个代码分析一下,在写代码之前我们先记得 无锁状态下的信息00000001,然后写一个偏向锁的例子看看结果 public static void main(String[] args) throws Exception { //-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 B b = new B(); out.println("befor lock"); out.println(ClassLayout.parseInstance(b).toPrintable()); synchronized (b){ out.println("lock ing"); out.println(ClassLayout.parseInstance(b).toPrintable()); out.println("after lock"); out.println(ClassLayout.parseInstance(b).toPrintable()); 上面这个程序只有一个线程去调用sync方法,故而讲道理应该是偏向锁,但是此时却是轻量级锁 而且你会发现最后输出的结果(第一个字节)依 然是00000001和无锁的时候一模一样,其实这是因为虚拟机在启动的时候对于偏向锁有延迟, 比如把上述代码当中加上 睡眠5秒的代码,结果就会不一样了, public static void main(String[] args) throws Exception { //-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 Thread.sleep(5000); B b = new B(); out.println("befor lock"); out.println(ClassLayout.parseInstance(b).toPrintable()); synchronized (b){ out.println("lock ing"); out.println(ClassLayout.parseInstance(b).toPrintable()); out.println("after lock"); out.println(ClassLayout.parseInstance(b).toPrintable()); 结果变成00000101.当然为了方便测试我们也可以直接通过JVM的参数来禁用延迟 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 结果是和睡眠5秒一样的. 想想为什么偏向锁会延迟?因为启动程序的时候,jvm会有很多操作,包括gc等等,jvm刚运行时存在大量的同步方法,很多都不是偏向锁, 而偏向锁升级为轻/重量级锁的很费时间和资源,因此jvm会延迟4秒左右再开启偏向锁. 那么为什么同步之前就是偏向锁呢?我猜想是jvm的原因,目前还不清楚. 需要注意的after lock,退出同步后依然保持了偏向信息 然后看下轻量级锁的对象头 static A a; public static void main(String[] args) throws Exception { a = new A(); out.println("befre lock"); out.println(ClassLayout.parseInstance(a).toPrintable()); synchronized (a){ out.println("lock ing"); out.println(ClassLayout.parseInstance(a).toPrintable()); out.println("after lock"); out.println(ClassLayout.parseInstance(a).toPrintable()); 关于重量锁首先看对象头 static A a; public static void main(String[] args) throws Exception { //Thread.sleep(5000); a = new A(); out.println("befre lock"); out.println(ClassLayout.parseInstance(a).toPrintable());//无锁 Thread t1= new Thread(){ public void run() { synchronized (a){ try { Thread.sleep(5000); System.out.println("t1 release"); } catch (InterruptedException e) { e.printStackTrace(); t1.start(); Thread.sleep(1000); out.println("t1 lock ing"); out.println(ClassLayout.parseInstance(a).toPrintable());//轻量锁 sync(); out.println("after lock"); out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁 System.gc(); out.println("after gc()"); out.println(ClassLayout.parseInstance(a).toPrintable());//无锁---gc public static void sync() throws InterruptedException { synchronized (a){ System.out.println("t1 main lock"); out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁  由上述实验可总结下图: 性能对比偏向锁和轻量级锁: public class A { int i=0; public synchronized void parse(){ //JOLExample6.countDownLatch.countDown(); 执行1000000000L次++操作 public class JOLExample4 { public static void main(String[] args) throws Exception { A a = new A(); long start = System.currentTimeMillis(); //调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能 //如果不出意外,结果灰常明显 for(int i=0;i&lt;1000000000L;i++){ a.parse(); long end = System.currentTimeMillis(); System.out.println(String.format("%sms", end - start)); 此时根据上面的测试可知是轻量级锁,看下结果 大概16秒 然后我们让偏向锁启动无延时,在启动一次 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0再看下结果 只需要2秒,速度提升了很多 再看下重量级锁的时间 static CountDownLatch countDownLatch = new CountDownLatch(1000000000); public static void main(String[] args) throws Exception { final A a = new A(); long start = System.currentTimeMillis(); //调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能 //如果不出意外,结果灰常明显 for(int i=0;i&lt;2;i++){ new Thread(){ @Override public void run() { while (countDownLatch.getCount() &gt; 0) { a.parse(); }.start(); countDownLatch.await(); long end = System.currentTimeMillis(); System.out.println(String.format("%sms", end - start)); 看下结果,大概31秒, 可以看出三种锁的消耗是差距很大的,这也是1.5以后synchronized优化的意义  需要注意的是如果对象已经计算了hashcode就不能偏向了 static A a; public static void main(String[] args) throws Exception { Thread.sleep(5000); a= new A(); a.hashCode(); out.println("befor lock"); out.println(ClassLayout.parseInstance(a).toPrintable()); synchronized (a){ out.println("lock ing"); out.println(ClassLayout.parseInstance(a).toPrintable()); out.println("after lock"); out.println(ClassLayout.parseInstance(a).toPrintable()); 原文地址https://www.cnblogs.com/lusaisai/p/12748869.html

Kubernetes 完整二进制部署(精品)

Kubernetes 完整二进制部署(精品)目录1、基础环境2、部署DNS3、准备自签证书4、部署Docker环境5、私有仓库Harbor部署6、部署Master节点6.1、部署Etcd集群6.2、部署kube-apiserver集群6.2.1、创建cliient证书6.2.2、签发kube-apiserver证书6.2.3、kube-apiserver配置6.3、L4反向代理6.3.1、部署Nginx6.3.2、部署keepalived6.4、部署controller-manager6.5、部署kube-scheduler7、部署Node节点服务7.1、部署Kubelet7.1.1、签发kubelet证书7.1.2、kubelet配置7.1.3、准备pause基础镜像7.1.4、创建kubelet启动脚本7.2、部署kube-proxy7.2.1、签发kube-proxy证书7.2.2、Kube-proxy配置7.2.3、创建kube-proxy启动脚本8、验证集群1、基础环境1.安装epel-release $ yum install epel-release -y 2.保证系统内核版本为3.10.x以上 $ uname -aLinux k8s-node01 3.10.0-693.el7.x86_643.关闭防火墙和selinux $ systemctl stop firewalld &amp;&amp; systemctl disable firewalld$ sed -i.bak 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config $ setenforce 04.时间同步 $ echo '#time sync by lidao at 2017-03-08' &gt;&gt;/var/spool/cron/root$ echo '/5 * /usr/sbin/ntpdate pool.ntp.org &gt;/dev/null 2&gt;&amp;1' &gt;&gt;/var/spool/cron/root$ crontab -l5.内核优化 $ cat &gt;&gt;/etc/sysctl.conf&lt;net.ipv4.tcp_fin_timeout = 2net.ipv4.ip_forward = 1net.ipv4.tcp_tw_reuse = 1net.ipv4.tcp_tw_recycle = 1net.ipv4.tcp_syncookies = 1net.ipv4.tcp_keepalive_time = 600net.ipv4.ip_local_port_range = 4000 65000net.ipv4.tcp_max_syn_backlog = 16384net.ipv4.tcp_max_tw_buckets = 36000net.ipv4.route.gc_timeout = 100net.ipv4.tcp_syn_retries = 1net.ipv4.tcp_synack_retries = 1net.core.somaxconn = 16384net.core.netdev_max_backlog = 16384net.ipv4.tcp_max_orphans = 16384EOF $ sysctl -p6.安装必要工具 $ yum install wget net-tools telnet tree nmap sysstat lrzsz dos2unix bind-utils -y 2、部署DNSbind9服务来实现DNS,在ingress中实现七层代理,在实验环境中就得绑定hosts方式实现访问,而且容器也没办法绑定hosts,这里通过DNS来实现。 1.安装bind9软件(hdss7-11) $ yum install bind -y2.主配置文件 $ vim /etc/named.conflisten-on port 53 { 10.4.7.11; }; # dns监听地址allow-query { any; }; # 允许所有主机访问dns服务forwarders { 10.4.7.2; }; # 指定上级dnsrecursion yes; # dns采用递归算法查询(另一种是迭代)dnssec-enable no; # 节约资源将其关闭dnssec-validation no; # 节约资源将其关闭配置文件语法校验 没有报错说明语法没问题 $ named-checkconf2.区域配置文件 定义了两个域,都为主DNS,运行本机update $ vim /etc/named.rfc1912.zoneszone "host.com" IN { type master; file "host.com.zone"; allow-update { 10.4.7.11; }; zone "od.com" IN { type master; file "od.com.zone"; allow-update { 10.4.7.11; }; };3.配置区域数据文件 /var/named/host.com.zone$ORIGIN host.com.$TTL 600 ; 10 minutes@ IN SOA dns.host.com. dnsadmin.host.com. ( 2019011001 ; serial 10800 ; refresh (3 hours) 900 ; retry (15 minutes) 604800 ; expire (1 week) 86400 ; minimum (1 day) NS dns.host.com. $TTL 60 ; 1 minutedns A 10.4.7.11HDSS7-11 A 10.4.7.11HDSS7-12 A 10.4.7.12HDSS7-21 A 10.4.7.21HDSS7-22 A 10.4.7.22HDSS7-200 A 10.4.7.200/var/named/od.com.zone$ORIGIN od.com.$TTL 600 ; 10 minutes@ IN SOA dns.od.com. dnsadmin.od.com. ( 2019011001 ; serial 10800 ; refresh (3 hours) 900 ; retry (15 minutes) 604800 ; expire (1 week) 86400 ; minimum (1 day) NS dns.od.com. $TTL 60 ; 1 minutedns A 10.4.7.114.启动dns服务 $ systemctl start named &amp;&amp; systemctl enable named5.验证是否可解析 $ dig -t A hdss7-21.host.com @10.4.7.11 +short10.4.7.21$ dig -t A hdss7-200.host.com @10.4.7.11 +short10.4.7.2006.DNS客户端配置 修改网卡dns方式 修改网卡配置文件DNS1 $ vim /etc/sysconfig/network-scripts/ifcfg-eth0DNS1=10.4.7.11 $ systemctl restart network 测试ping $ ping baidu.comPING baidu.com (39.156.69.79) 56(84) bytes of data.64 bytes from 39.156.69.79 (39.156.69.79): icmp_seq=1 ttl=128 time=47.0 ms64 bytes from 39.156.69.79 (39.156.69.79): icmp_seq=2 ttl=128 time=48.3 ms $ ping hdss7-21.host.comPING HDSS7-21.host.com (10.4.7.21) 56(84) bytes of data.64 bytes from 10.4.7.21 (10.4.7.21): icmp_seq=1 ttl=64 time=0.821 ms64 bytes from 10.4.7.21 (10.4.7.21): icmp_seq=2 ttl=64 time=0.598 ms添加search(短域名)$ vim /etc/resolv.conf Generated by NetworkManager search host.comnameserver 10.4.7.2 Ping短域名 $ ping hdss7-200PING HDSS7-200.host.com (10.4.7.200) 56(84) bytes of data.64 bytes from 10.4.7.200 (10.4.7.200): icmp_seq=1 ttl=64 time=1.18 ms64 bytes from 10.4.7.200 (10.4.7.200): icmp_seq=2 ttl=64 time=0.456 ms3、准备自签证书运维主机hdss7-200.host.com上: 1.安装CFSSL $ wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -O /usr/bin/cfssl$ wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -O /usr/bin/cfssl-json$ wget https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -O /usr/bin/cfssl-certinfo$ chmod +x /usr/bin/cfssl*关于cfssl工具:cfssl:证书签发的主要工具cfssl-json:将cfssl生成的整数(json格式)变为文件承载式证书cfssl-certinfo:验证证书的信息2.创建生成CA证书签名请求(csr)的json配置文件 自签证书会有个根证书ca(需权威机构签发/可自签) $ vim /opt/certs/ca-csr.json{ "CN": "Sky", "hosts": [ "key": { "algo": "rsa", "size": 2048 "names": [ "C": "CN", "ST": "beijing", "L": "beijing", "O": "od", "OU": "ops" "ca": { "expiry": "175200h" }CN:浏览器使用该字段验证网站是否合法,一般写的是域名,非常重要 ST:州/省 L:地区/城市 O:组织名称/公司名称 OU:组织单位名称,公司部门 3.生成CA证书和私钥 $ cfssl gencert -initca ca-csr.json | cfssl-json -bare ca2020/01/10 13:58:49 [INFO] generating a new CA key and certificate from CSR2020/01/10 13:58:49 [INFO] generate received request2020/01/10 13:58:49 [INFO] received CSR2020/01/10 13:58:49 [INFO] generating key: rsa-20482020/01/10 13:58:49 [INFO] encoded CSR2020/01/10 13:58:49 [INFO] signed certificate with serial number 214125439771303219718649555160058070055859759808 $ lltotal 16-rw-r--r-- 1 root root 328 Jan 10 13:53 ca-csr.json # 请求文件-rw------- 1 root root 1675 Jan 10 13:58 ca-key.pem # 私钥-rw-r--r-- 1 root root 993 Jan 10 13:58 ca.csr -rw-r--r-- 1 root root 1346 Jan 10 13:58 ca.pem # 证书4、部署Docker环境hdss7-200.host.com,hdss7-21.host.com,hdss7-22.host.com上: 1.一键安装Docker-ce $ curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun$ docker version2.配置文件 $ mkdir /etc/docker/ $ mkdir -p /data/docker$ vim /etc/docker/daemon.json{ "graph": "/data/docker", "storage-driver": "overlay2", "insecure-registries": ["registry.access.redhat.com","quay.io","harbor.od.com"], "registry-mirrors": ["https://q2gr04ke.mirror.aliyuncs.com"], "bip": "172.7.21.1/24", "exec-opts": ["native.cgroupdriver=systemd"], "live-restore": true}bip:172.7.x.1/24,x按照宿主机IP地址最后一位来设置 3.启动docker $ systemctl restart docker.service &amp;&amp; systemctl enable docker.service5、私有仓库Harbor部署hdss7-200.host.com上: 1.下载软件二进制包并解压 https://github.com/goharbor/harbor $ tar xf harbor-offline-installer-v1.8.1.tgz -C /opt/$ mv /opt/harbor/ /opt/harbor-v1.8.1$ ln -s /opt/harbor-v1.8.1/ /opt/harbor2.配置文件 $ vim /opt/harbor/harbor.ymlhostname: harbor.od.comhttp: port: 180data_volume: /data/harborlocation: /data/harbor/logs创建相应目录 $ mkdir -p /data/harbor/logs3.安装docker-compose 用于编排harbor $ yum install docker-compose -y4.启动harbor $ sh /opt/harbor/install.sh$ docker-compose ps5.基于Nginx实现代理访问Harbor $ yum install nginx -y $ vim /etc/nginx/conf.d/harbor.od.com.confserver { listen 80; server_name harbor.od.com; client_max_body_size 1000m; location / { proxy_pass http://127.0.0.1:180; }配置说明:用户访问url:harbor.od.com 端口80 将其流量代理到 127.0.0.1:180 启动nginx $ nginx -t$ systemctl start nginx &amp;&amp; systemctl enable nginx6.hdss7-11上添加dns A记录 $ vi /var/named/od.com.zoneharbor A 10.4.7.200注意serial前滚一个序号 重启dns并测试 $ systemctl restart named$ dig -t A harbor.od.com +short10.4.7.2007.浏览器访问:harbor.od.com 用户名:admin、密码:Harbor12345 8.harbor上新建一个名:public 公开项目 9.从docker.io拉取nginx镜像 $ docker pull nginx:1.7.9 $ docker pull docker.io/library/nginx:1.7.9将从公网下载的nginx打上刚才创建的harbor仓库下public项目的tag 找到nginx image id将其打上new tag $ docker tag 84581e99d807 harbor.od.com/public/nginx:v1.7.9 需先登录harbor $ docker login harbor.od.comUsername: adminPassword: 然后在推送镜像到私有仓库 $ docker push harbor.od.com/public/nginx:v1.7.96、部署Master节点6.1、部署Etcd集群集群规划 主机名 角色 iphdss7-12.host.com etcd lead 10.4.7.12hdss7-21.host.com etcd follow 10.4.7.21hdss7-22.host.com etcd follow 10.4.7.22注意:这里部署文档以hdss7-12.host.com主机为例,另外两台主机安装部署方法类似 1.创建基于根证书的config配置文件 运维主机hdss7-200上: $ vim /opt/certs/ca-config.json{ "signing": { "default": { "expiry": "175200h" "profiles": { "server": { "expiry": "175200h", "usages": [ "signing", "key encipherment", "server auth" "client": { "expiry": "175200h", "usages": [ "signing", "key encipherment", "client auth" "peer": { "expiry": "175200h", "usages": [ "signing", "key encipherment", "server auth", "client auth" }证书类型 client:客户端使用,用于服务端认证客户端,例如etcdctl、etcd proxy、fleetctl、docker客户端。 server:服务端使用,客户端以此验证服务端身份,例如docker服务端、kube-apiserver peer:双向证书,用于etcd集群成员间通信 2.创建生成etcd自签证书签名请求(csr)的json配置文件 运维主机hdss7-200上: $ vim /opt/certs/etcd-peer-csr.json{ "CN": "k8s-etcd", "hosts": [ "10.4.7.11", "10.4.7.12", "10.4.7.21", "10.4.7.22" "key": { "algo": "rsa", "size": 2048 "names": [ "C": "CN", "ST": "beijing", "L": "beijing", "O": "od", "OU": "ops" }hosts:添加部署etcd机器的IP地址,尽量多预留几个 3.生成etcd证书和私钥 运维主机hdss7-200上: $ cd /opt/certs$ cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer etcd-peer-csr.json |cfssl-json -bare etcd-peer2020/01/10 15:05:30 [INFO] generate received request2020/01/10 15:05:30 [INFO] received CSR2020/01/10 15:05:30 [INFO] generating key: rsa-20482020/01/10 15:05:30 [INFO] encoded CSR2020/01/10 15:05:30 [INFO] signed certificate with serial number 2574197595027130875803445990359134112255715441602020/01/10 15:05:30 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for For more information see the Baseline Requirements for the Issuance and Management Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org); specifically, section 10.2.3 ("Information Requirements").检查生成的证书、私钥 $ ll etcd-peer*-rw-r--r-- 1 root root 364 Jan 10 15:03 etcd-peer-csr.json-rw------- 1 root root 1675 Jan 10 15:05 etcd-peer-key.pem-rw-r--r-- 1 root root 1062 Jan 10 15:05 etcd-peer.csr-rw-r--r-- 1 root root 1428 Jan 10 15:05 etcd-peer.pem4.创建etcd用户 hdss7-12.host.com上: $ useradd -s /sbin/nologin -M etcd5.下载软件、解压,做软连接 hdss7-12.host.com上: etcd下载地址 $ tar xf etcd-v3.1.20-linux-amd64.tar.gz -C /opt/$ mv /opt/etcd-v3.1.20-linux-amd64/ /opt/etcd-v3.1.20$ ln -s /opt/etcd-v3.1.20/ /opt/etcd6.创建目录,拷贝证书、私钥 hdss7-12.host.com上: 创建目录$ mkdir -p /opt/etcd/certs /data/etcd /data/logs/etcd-server拷贝证书将运维主机上生成的ca.pem、etcd-peer-key.pem、etcd-peer.pem拷贝到/opt/etcd/certs目录中,注意私钥文件权限600 $ ll -ltotal 12-rw-r--r-- 1 root root 1346 Jan 27 12:04 ca.pem-rw------- 1 root root 1675 Jan 27 12:03 etcd-peer-key.pem-rw-r--r-- 1 root root 1432 Jan 27 12:03 etcd-peer.pem修改权限$ chown -R etcd.etcd /opt/etcd-v3.1.20 /data/etcd/ /data/logs/etcd-server/必须使用etcd用户启动 7.创建etcd服务启动脚本 hdss7-12.host.com上: $ vim /opt/etcd/etcd-server-startup.sh !/bin/sh ./etcd --name etcd-server-7-12 \ --data-dir /data/etcd/etcd-server \ --listen-peer-urls https://10.4.7.12:2380 \ --listen-client-urls https://10.4.7.12:2379,http://127.0.0.1:2379 \ --quota-backend-bytes 8000000000 \ --initial-advertise-peer-urls https://10.4.7.12:2380 \ --advertise-client-urls https://10.4.7.12:2379,http://127.0.0.1:2379 \ --initial-cluster etcd-server-7-12=https://10.4.7.12:2380,etcd-server-7-21=https://10.4.7.21:2380,etcd-server-7-22=https://10.4.7.22:2380 \ --ca-file ./certs/ca.pem \ --cert-file ./certs/etcd-peer.pem \ --key-file ./certs/etcd-peer-key.pem \ --client-cert-auth \ --trusted-ca-file ./certs/ca.pem \ --peer-ca-file ./certs/ca.pem \ --peer-cert-file ./certs/etcd-peer.pem \ --peer-key-file ./certs/etcd-peer-key.pem \ --peer-client-cert-auth \ --peer-trusted-ca-file ./certs/ca.pem \ --log-output stdout 配置参数说明 参数 说明--listen-peer-urls 本member侧使用,用于监听其他member发送信息的地址。ip为全0代表监听本member侧所有接口--listen-client-urls 本member侧使用,用于监听etcd客户发送信息的地址。ip为全0代表监听本member侧所有接口--initial-advertise-peer-urls 其他member使用,其他member通过该地址与本member交互信息。一定要保证从其他member能可访问该地址。静态配置方式下,该参数的value一定要同时在--initial-cluster参数中存在。memberID的生成受--initial-cluster-token和--initial-advertise-peer-urls影响。--advertise-client-urls etcd客户使用,客户通过该地址与本member交互信息。一定要保证从客户侧能可访问该地址--initial-cluster etcd集群所有节点配置,多个用逗号隔开-quota-backend-bytes 指定etcd存储配额超过指定大小后引发报警--client-cert-auth 启动客户端证书进行身份验证--log-output 指定“ stdout”或“ stderr”以跳过日志记录,即使在systemd或逗号分隔的输出目标列表下运行时也是如此。详细请点击 给脚本添加执行权限 $ chmod +x /opt/etcd/etcd-server-startup.sh8.创建etcd-server的启动配置 hdss7-12.host.com上: 安装supervisor(优势:自动拉起挂掉的程序) $ yum install supervisor -y$ systemctl start supervisord &amp;&amp; systemctl enable supervisord将etcd启动脚本交给supervisor管理 $ vim /etc/supervisord.d/etcd-server.ini[program:etcd-server-7-12]command=/opt/etcd/etcd-server-startup.sh ; the program (relative uses PATH, can take args)numprocs=1 ; number of processes copies to start (def 1)directory=/opt/etcd ; directory to cwd to before exec (def no cwd)autostart=true ; start at supervisord start (default: true)autorestart=true ; retstart at unexpected quit (default: true)startsecs=30 ; number of secs prog must stay running (def. 1)startretries=3 ; max # of serial start failures (default 3)exitcodes=0,2 ; 'expected' exit codes for process (default 0,2)stopsignal=QUIT ; signal used to kill process (default TERM)stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)user=etcd ; setuid to this UNIX account to run the programredirect_stderr=true ; redirect proc stderr to stdout (default false)stdout_logfile=/data/logs/etcd-server/etcd.stdout.log ; stdout log path, NONE for none; default AUTOstdout_logfile_maxbytes=64MB ; max # logfile bytes b4 rotation (default 50MB)stdout_logfile_backups=4 ; # of stdout logfile backups (default 10)stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)stdout_events_enabled=false ; emit events on stdout writes (default false)killasgroup=truestopasgroup=true注意:etcd集群各主机启动配置略有不同,配置其它节点时注意修改; 9.启动etcd hdss7-12.host.com上: $ supervisorctl updateetcd-server-7-12: added process group检查是否启动 $ supervisorctl statusetcd-server-7-12 RUNNING pid 5029, uptime 0:02:11$ netstat -lntup | grep "etcd"tcp 0 0 10.4.7.12:2379 0.0.0.0:* LISTEN 5030/./etcdtcp 0 0 127.0.0.1:2379 0.0.0.0:* LISTEN 5030/./etcdtcp 0 0 10.4.7.12:2380 0.0.0.0:* LISTEN 5030/./etcd10.检查集群状态(必须在三节点起来后) $ /opt/etcd/etcdctl cluster-healthmember 988139385f78284 is healthy: got healthy result from http://127.0.0.1:2379member 5a0ef2a004fc4349 is healthy: got healthy result from http://127.0.0.1:2379member f4a0cb0a765574a8 is healthy: got healthy result from http://127.0.0.1:2379cluster is healthy检查集群角色 $ ./etcdctl member list988139385f78284: name=etcd-server-7-22 peerURLs=https://10.4.7.22:2380 clientURLs=http://127.0.0.1:2379,https://10.4.7.22:2379 isLeader=false5a0ef2a004fc4349: name=etcd-server-7-21 peerURLs=https://10.4.7.21:2380 clientURLs=http://127.0.0.1:2379,https://10.4.7.21:2379 isLeader=falsef4a0cb0a765574a8: name=etcd-server-7-12 peerURLs=https://10.4.7.12:2380 clientURLs=http://127.0.0.1:2379,https://10.4.7.12:2379 isLeader=true6.2、部署kube-apiserver集群集群规划 主机名 角色 iphdss7-21.host.com kube-apiserver 10.4.7.21hdss7-22.host.com kube-apiserver 10.4.7.22hdss7-11.host.com 4层负载均衡 10.4.7.11hdss7-12.host.com 4层负载均衡 10.4.7.12注意:这里10.4.7.11和10.4.7.12使用nginx做4层负载均衡,用keepalived跑一个VIP:10.4.7.10,代理两个kube-apiserver,实现高可用 这类部署文档以hdss7-21.host.com主机为例,另外一台运算节点部署方法类似 下载软件,解压,做软连接 hdss7-21.host.com上: kubernetes官方Github地址 kuberneetes下载地址 $ tar xf /opt/src/kubernetes-server-linux-amd64-v1.15.2.tar.gz -C /opt/$ mv /opt/kubernetes/ /opt/kubernetes-v1.15.2$ ln -s /opt/kubernetes-v1.15.2/ /opt/kubernetes6.2.1、创建cliient证书运维主机hdss7-200上: 1创建生成证书签名请求(csr)的json配置文件 $ vim /opt/certs/client-csr.json{ "CN": "k8s-node", "hosts": [ "key": { "algo": "rsa", "size": 2048 "names": [ "C": "CN", "ST": "beijing", "L": "beijing", "O": "od", "OU": "ops" }2.生成client证书和私钥 $ cd /opt/certs/$ cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client client-csr.json |cfssl-json -bare client2020/01/10 16:16:35 [INFO] generate received request2020/01/10 16:16:35 [INFO] received CSR2020/01/10 16:16:35 [INFO] generating key: rsa-20482020/01/10 16:16:36 [INFO] encoded CSR2020/01/10 16:16:36 [INFO] signed certificate with serial number 2946508907328814785971504795452208445430076275122020/01/10 16:16:36 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for For more information see the Baseline Requirements for the Issuance and Management Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org); specifically, section 10.2.3 ("Information Requirements").3.检查生成的证书、私钥 $ ll client*-rw-r--r-- 1 root root 280 Jan 10 16:15 client-csr.json-rw------- 1 root root 1679 Jan 10 16:16 client-key.pem-rw-r--r-- 1 root root 993 Jan 10 16:16 client.csr-rw-r--r-- 1 root root 1363 Jan 10 16:16 client.pem6.2.2、签发kube-apiserver证书运维主机hdss7-200上: 1.创建生成证书签名请求(csr)的json配置文件 $ vim /opt/certs/apiserver-csr.json{ "CN": "k8s-apiserver", "hosts": [ "127.0.0.1", "192.168.0.1", "kubernetes.default", "kubernetes.default.svc", "kubernetes.default.svc.cluster", "kubernetes.default.svc.cluster.local", "10.4.7.10", "10.4.7.21", "10.4.7.22", "10.4.7.23" "key": { "algo": "rsa", "size": 2048 "names": [ "C": "CN", "ST": "beijing", "L": "beijing", "O": "od", "OU": "ops" hosts 字段指定授权使用该证书的 IP 或域名列表,这里列出了 VIP 、apiserver节点 IP、kubernetes 服务 IP 和域名;域名最后字符不能是 . (如不能为kubernetes.default.svc.cluster.local. ),否则解析时失败,提示: x509:cannot parse dnsName "kubernetes.default.svc.cluster.local." ;如果使用非 cluster.local 域名,如 opsnull.com ,则需要修改域名列表中的最后两个域名为: kubernetes.default.svc.opsnull 、 kubernetes.default.svc.opsnull.comkubernetes 服务 IP 是 apiserver 自动创建的,一般是 --service-cluster-ip-range 参数指定的网段的第一个IP,后续可以通过如下命令获取:$ kubectl get svc kubernetesNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkubernetes ClusterIP 192.168.0.1 443/TCP 4d2.生成api-server证书和私钥 $ cd /opt/certs$ cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server apiserver-csr.json |cfssl-json -bare apiserver2020/01/10 16:21:06 [INFO] generate received request2020/01/10 16:21:06 [INFO] received CSR2020/01/10 16:21:06 [INFO] generating key: rsa-20482020/01/10 16:21:07 [INFO] encoded CSR2020/01/10 16:21:07 [INFO] signed certificate with serial number 5333989707018849513209702287650723098755445692052020/01/10 16:21:07 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for For more information see the Baseline Requirements for the Issuance and Management Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org); specifically, section 10.2.3 ("Information Requirements").3.检查生成的证书、私钥 $ ll apiserver*-rw-r--r-- 1 root root 566 Jan 10 16:19 apiserver-csr.json-rw------- 1 root root 1679 Jan 10 16:21 apiserver-key.pem-rw-r--r-- 1 root root 1249 Jan 10 16:21 apiserver.csr-rw-r--r-- 1 root root 1598 Jan 10 16:21 apiserver.pem6.2.3、kube-apiserver配置hdss7-21上: 1.创建目录存放证书和私钥以及配置文件 $ mkdir /opt/kubernetes/server/bin/cert /opt/kubernetes/server/bin/conf/opt/kubernetes/server/bin/cert:存放证书 /opt/kubernetes/server/bin/conf:存放启动配置文件 2.拷贝证书、私钥,注意私钥文件属性600 $ ll # 三套证书total 24-rw------- 1 root root 1679 Jan 10 16:32 apiserver-key.pem-rw-r--r-- 1 root root 1598 Jan 10 16:32 apiserver.pem-rw------- 1 root root 1675 Jan 10 16:32 ca-key.pem-rw-r--r-- 1 root root 1346 Jan 10 16:32 ca.pem-rw------- 1 root root 1679 Jan 10 16:32 client-key.pem-rw-r--r-- 1 root root 1363 Jan 10 16:32 client.pem3.创建api-server审计策略文件 $ vi /opt/kubernetes/server/bin/conf/audit.yamlapiVersion: audit.k8s.io/v1beta1 # This is required.kind: Policy Don't generate audit events for all requests in RequestReceived stage. omitStages: "RequestReceived"rules: # Log pod changes at RequestResponse level level: RequestResponseresources: group: "" # Resource "pods" doesn't match requests to any subresource of pods, # which is consistent with the RBAC policy. resources: ["pods"] # Log "pods/log", "pods/status" at Metadata level level: Metadataresources: group: "" resources: ["pods/log", "pods/status"] # Don't log requests to a configmap called "controller-leader" level: Noneresources: group: "" resources: ["configmaps"] resourceNames: ["controller-leader"] # Don't log watch requests by the "system:kube-proxy" on endpoints or services level: Noneusers: ["system:kube-proxy"]verbs: ["watch"]resources: group: "" # core API group resources: ["endpoints", "services"] # Don't log authenticated requests to certain non-resource URL paths. level: NoneuserGroups: ["system:authenticated"]nonResourceURLs: "/api*" # Wildcard matching. "/version" # Log the request body of configmap changes in kube-system. level: Requestresources: group: "" # core API group resources: ["configmaps"] This rule only applies to resources in the "kube-system" namespace. The empty string "" can be used to select non-namespaced resources. namespaces: ["kube-system"] # Log configmap and secret changes in all other namespaces at the Metadata level. level: Metadataresources: group: "" # core API group resources: ["secrets", "configmaps"] # Log all other resources in core and extensions at the Request level. level: Requestresources: group: "" # core API group group: "extensions" # Version of group should NOT be included. # A catch-all rule to log all other requests at the Metadata level. level: Metadata # Long-running requests like watches that fall under this rule will not # generate an audit event in RequestReceived. omitStages: "RequestReceived"4.创建启动脚本 $ vim /opt/kubernetes/server/bin/kube-apiserver.sh !/bin/bash ./kube-apiserver \ --apiserver-count 2 \ --insecure-port 8080 \ --secure-port 6443 \ --audit-log-path /data/logs/kubernetes/kube-apiserver/audit-log \ --audit-policy-file ./conf/audit.yaml \ --authorization-mode RBAC \ --client-ca-file ./cert/ca.pem \ --requestheader-client-ca-file ./cert/ca.pem \ --enable-admission-plugins NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota \ --etcd-cafile ./cert/ca.pem \ --etcd-certfile ./cert/client.pem \ --etcd-keyfile ./cert/client-key.pem \ --etcd-servers https://10.4.7.12:2379,https://10.4.7.21:2379,https://10.4.7.22:2379 \ --service-account-key-file ./cert/ca-key.pem \ --service-cluster-ip-range 192.168.0.0/16 \ --service-node-port-range 3000-29999 \ --target-ram-mb=1024 \ --kubelet-client-certificate ./cert/client.pem \ --kubelet-client-key ./cert/client-key.pem \ --log-dir /data/logs/kubernetes/kube-apiserver \ --tls-cert-file ./cert/apiserver.pem \ --tls-private-key-file ./cert/apiserver-key.pem \ --v 2service-cluster-ip-range:指定service IP(cluster ip)范围 配置参数说明 参数 说明--apiserver-count 指定集群运行模式,多台 kube-apiserver 会通过 leader选举产生一个工作节点,其它节点处于阻塞状态--authorization-mode 开启指定授权模式,拒绝未授权的请求,默认值:AlwaysAllow;以逗号分隔的列表:AlwaysAllow,AlwaysDeny,ABAC,Webhook,RBAC,Node--enable-admission-plugins 启用指定插件--etcd-servers etcd服务器列表(格式://ip:port),逗号分隔--service-account-key-file 包含PEM编码的x509 RSA或ECDSA私有或者公共密钥的文件。用于验证service account token。指定的文件可以包含多个值。参数可以被指定多个不同的文件。如未指定,--tls-private-key-file将被使用。如果提供了--service-account-signing-key,则必须指定该参数--service-cluster-ip-range CIDR表示IP范围,用于分配服务集群IP(service ip)。不能与分配给pod节点的IP重叠 (default 10.0.0.0/24)--service-node-port-range 为NodePort服务保留的端口范围。默认值 30000-32767--kubelet-client-certificate、kubelet-client-key 如果指定,则使用 https 访问 kubelet APIs;需要为证书对应的用户(上面 kubernetes*.pem 证书的用户为 kubernetes) 用户定义 RBAC 规则,否则访问 kubelet API 时提示未授权--tls-cert-file、tls-private-key-file 使用 https 输出 metrics 时使用的 Server 证书和秘钥--insecure-port HTTP服务,默认端口8080,默认IP是本地主机,修改标识--insecure-bind-address,在HTTP中没有认证和授权检查--secure-port HTTPS服务,默认端口6443,默认IP是首个非本地主机的网络接口,修改标识--bind-address,设置证书和秘钥的标识,--tls-cert-file,--tls-private-key-file,认证方式,令牌文件或者客户端证书,使用基于策略的授权方式给脚本添加执行权限 $ chmod +x /opt/kubernetes/server/bin/kube-apiserver.sh5.创建api-server的启动配置 $ vi /etc/supervisord.d/kube-apiserver.ini[program:kube-apiserver-7-21]command=/opt/kubernetes/server/bin/kube-apiserver.sh ; the program (relative uses PATH, can take args)numprocs=1 ; number of processes copies to start (def 1)directory=/opt/kubernetes/server/bin ; directory to cwd to before exec (def no cwd)autostart=true ; start at supervisord start (default: true)autorestart=true ; retstart at unexpected quit (default: true)startsecs=30 ; number of secs prog must stay running (def. 1)startretries=3 ; max # of serial start failures (default 3)exitcodes=0,2 ; 'expected' exit codes for process (default 0,2)stopsignal=QUIT ; signal used to kill process (default TERM)stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)user=root ; setuid to this UNIX account to run the programredirect_stderr=true ; redirect proc stderr to stdout (default false)stdout_logfile=/data/logs/kubernetes/kube-apiserver/apiserver.stdout.log ; stderr log path, NONE for none; default AUTOstdout_logfile_maxbytes=64MB ; max # logfile bytes b4 rotation (default 50MB)stdout_logfile_backups=4 ; # of stdout logfile backups (default 10)stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)stdout_events_enabled=false ; emit events on stdout writes (default false)killasgroup=truestopasgroup=true6.创建日志目录 $ mkdir -p /data/logs/kubernetes/kube-apiserver7.启动并检查 $ supervisorctl update$ supervisorctl statusetcd-server-7-22 RUNNING pid 4013, uptime 1:12:36kube-apiserver-7-22 RUNNING pid 4596, uptime 0:00:318.查看api-server端口 $ netstat -lntup | egrep "8080|6443"tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN 20375/./kube-apiser tcp6 0 0 :::6443 :::* LISTEN 20375/./kube-apiser6.3、L4反向代理hdss7-11和hdss7-12上基于nginx实现L4反向代理调度到后端的kubernetes api-server: 所有Node节点的k8s组件:kubelet,kube-proxy会去访问https://10.4.7.10:7443这个地址,并携带证书 6.3.1、部署Nginx1.安装nginx $ yum install nginx -y 2.nginx配置文件 $ vim /etc/nginx/nginx.conf # 黏贴到http标签外stream { # kubernetes api-server ip地址以及https端口 upstream kube-apiserver { server 10.4.7.21:6443 max_fails=3 fail_timeout=30s; server 10.4.7.22:6443 max_fails=3 fail_timeout=30s; # 监听7443端口,将其接收的流量转发至指定proxy_pass server { listen 7443; proxy_connect_timeout 2s; proxy_timeout 900s; proxy_pass kube-apiserver; }3.启动nginx $ systemctl start nginx &amp;&amp; systemctl enable nginx6.3.2、部署keepalived1.安装keepalived $ yum install keepalived -y2.监听脚本 $ vi /etc/keepalived/check_port.sh !/bin/bash keepalived 监控端口脚本 使用方法: 在keepalived的配置文件中 vrrp_script check_port {#创建一个vrrp_script脚本,检查配置 script "/etc/keepalived/check_port.sh 6379" #配置监听的端口 interval 2 #检查脚本的频率,单位(秒) CHK_PORT=$1if [ -n "$CHK_PORT" ];then PORT_PROCESS=`ss -lnt|grep $CHK_PORT|wc -l` if [ $PORT_PROCESS -eq 0 ];then echo "Port $CHK_PORT Is Not Used,End." exit 1 echo "Check Port Cant Be Empty!" fi添加可执行权限 $ chmod +x /etc/keepalived/check_port.sh3.keepalived主配置文件 $ vi /etc/keepalived/keepalived.conf! Configuration File for keepalived global_defs { router_id 10.4.7.11} vrrp_script chk_nginx { # 调用脚本检测nginx监听的7443端口是否存在 script "/etc/keepalived/check_port.sh 7443" interval 2 weight -20 vrrp_instance VI_1 { state MASTER interface eth0 virtual_router_id 251 priority 100 advert_int 1 # 当前主机IP mcast_src_ip 10.4.7.11 nopreempt # 高可用认证 authentication { auth_type PASS auth_pass 11111111 track_script { chk_nginx # 虚拟IP virtual_ipaddress { 10.4.7.10 }4.keepalived备配置文件 $ vi /etc/keepalived/keepalived.conf! Configuration File for keepalivedglobal_defs { router_id 10.4.7.12 }vrrp_script chk_nginx { script "/etc/keepalived/check_port.sh 7443" interval 2 weight -20 }vrrp_instance VI_1 { state BACKUP interface eth0 virtual_router_id 251 mcast_src_ip 10.4.7.12 priority 90 advert_int 1 authentication { auth_type PASS auth_pass 11111111 track_script { chk_nginx virtual_ipaddress { 10.4.7.10 }5.启动 $ systemctl start keepalived.service &amp;&amp; systemctl enable keepalived.service6.4、部署controller-managerhdss7-21.host.com和hdss7-22.host.com都部署了api-server,并且暴露了127.0.0.1:8080端口,也就是只能当前机器访问,那么controller-manager也是部署到当前机器,那就可以通过非安全端口8080直接访问到本机的api-server,即访问快捷/速度快又不需要证书认证。 主机名 角色 iphdss7-21.host.com controller-manager 10.4.7.21hdss7-22.host.com controller-manager 10.4.7.22注意:这里部署文档以hdds7-21.host.com主机为例,另外一台运算节点安装部署方法类似 1.创建启动脚本 $ vim /opt/kubernetes/server/bin/kube-controller-manager.sh !/bin/sh ./kube-controller-manager \ --cluster-cidr 172.7.0.0/16 \ --leader-elect true \ --log-dir /data/logs/kubernetes/kube-controller-manager \ --master http://127.0.0.1:8080 \ --service-account-private-key-file ./cert/ca-key.pem \ --service-cluster-ip-range 192.168.0.0/16 \ --root-ca-file ./cert/ca.pem \ --v 2配置参数说明 参数 说明--cluster-cidr 集群中Pod的CIDR范围,--master kubernetes api server的地址,将会覆盖kubeconfig设置的值--service-cluster-ip-range 集群service的cidr范围,需要--allocate-node-cidrs设置为true--leader-elect 多个master情况设置为true保证高可用,进行leader选举--leader-elect-lease-duration duration 当leader-elect设置为true生效,选举过程中非leader候选等待选举的时间间隔(default 15s)--leader-elect-renew-deadline duration eader选举过程中在停止leading,再次renew时间间隔,小于或者等于leader-elect-lease-duration duration,也是leader-elect设置为true生效(default 10s)--leader-elect-retry-period duration 当leader-elect设置为true生效,获取leader或者重新选举的等待间隔(default 2s)2.调整文件权限,创建日志存放目录 $ chmod +x /opt/kubernetes/server/bin/kube-controller-manager.sh$ mkdir -p /data/logs/kubernetes/kube-controller-manager3.创建controller-manager的启动配置 $ vi /etc/supervisord.d/kube-conntroller-manager.ini[program:kube-controller-manager-7-21]command=/opt/kubernetes/server/bin/kube-controller-manager.sh ; the program (relative uses PATH, can take args)numprocs=1 ; number of processes copies to start (def 1)directory=/opt/kubernetes/server/bin ; directory to cwd to before exec (def no cwd)autostart=true ; start at supervisord start (default: true)autorestart=true ; retstart at unexpected quit (default: true)startsecs=30 ; number of secs prog must stay running (def. 1)startretries=3 ; max # of serial start failures (default 3)exitcodes=0,2 ; 'expected' exit codes for process (default 0,2)stopsignal=QUIT ; signal used to kill process (default TERM)stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)user=root ; setuid to this UNIX account to run the programredirect_stderr=true ; redirect proc stderr to stdout (default false)stdout_logfile=/data/logs/kubernetes/kube-controller-manager/controller.stdout.log ; stderr log path, NONE for none; default AUTOstdout_logfile_maxbytes=64MB ; max # logfile bytes b4 rotation (default 50MB)stdout_logfile_backups=4 ; # of stdout logfile backups (default 10)stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)stdout_events_enabled=false ; emit events on stdout writes (default false)killasgroup=truestopasgroup=true4.启动并检查 $ supervisorctl update$ supervisorctl statusetcd-server-7-21 RUNNING pid 4148, uptime 2:07:47kube-apiserver-7-21 RUNNING pid 4544, uptime 1:02:36kube-controller-manager-7-21 RUNNING pid 4690, uptime 0:00:326.5、部署kube-schedulerhdss7-21.host.com和hdss7-22.host.com都部署了api-server,并且暴露了127.0.0.1:8080端口,也就是只能当前机器访问,那么kube-scheduler也是部署到当前机器,那就可以通过非安全端口8080直接访问到本机的api-server,即访问快捷/速度快又不需要证书认证。 主机名 角色 iphdss7-21.host.com kube-scheduler 10.4.7.21hdss7-22.host.com kube-scheduler 10.4.7.22注意:这里部署文档以hdds7-21.host.com主机为例,另外一台运算节点安装部署方法类似 1.创建启动脚本 $ vim /opt/kubernetes/server/bin/kube-scheduler.sh !/bin/sh ./kube-scheduler \ --leader-elect \ --log-dir /data/logs/kubernetes/kube-scheduler \ --master http://127.0.0.1:8080 \ --v 2master:指定api-server 2.调整文件权限,创建目录 $ chmod +x /opt/kubernetes/server/bin/kube-scheduler.sh$ mkdir -p /data/logs/kubernetes/kube-scheduler3.创建controller-manager的启动配置 $ vi /etc/supervisord.d/kube-scheduler.ini[program:kube-scheduler-7-21]command=/opt/kubernetes/server/bin/kube-scheduler.sh ; the program (relative uses PATH, can take args)numprocs=1 ; number of processes copies to start (def 1)directory=/opt/kubernetes/server/bin ; directory to cwd to before exec (def no cwd)autostart=true ; start at supervisord start (default: true)autorestart=true ; retstart at unexpected quit (default: true)startsecs=30 ; number of secs prog must stay running (def. 1)startretries=3 ; max # of serial start failures (default 3)exitcodes=0,2 ; 'expected' exit codes for process (default 0,2)stopsignal=QUIT ; signal used to kill process (default TERM)stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)user=root ; setuid to this UNIX account to run the programredirect_stderr=true ; redirect proc stderr to stdout (default false)stdout_logfile=/data/logs/kubernetes/kube-scheduler/scheduler.stdout.log ; stderr log path, NONE for none; default AUTOstdout_logfile_maxbytes=64MB ; max # logfile bytes b4 rotation (default 50MB)stdout_logfile_backups=4 ; # of stdout logfile backups (default 10)stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)stdout_events_enabled=false ; emit events on stdout writes (default false)killasgroup=truestopasgroup=true4.启动并检查 $ supervisorctl update$ supervisorctl statusetcd-server-7-21 RUNNING pid 4148, uptime 2:11:12kube-apiserver-7-21 RUNNING pid 4544, uptime 1:06:01kube-controller-manager-7-21 RUNNING pid 4690, uptime 0:03:57kube-scheduler-7-21 RUNNING pid 4727, uptime 0:00:325.检查集群健康状态 $ ln -s /opt/kubernetes/server/bin/kubectl /usr/bin/kubectl$ kubectl get csNAME STATUS MESSAGE ERRORcontroller-manager Healthy okscheduler Healthy oketcd-2 Healthy {"health": "true"}etcd-0 Healthy {"health": "true"}etcd-1 Healthy {"health": "true"}7、部署Node节点服务7.1、部署Kubelet集群规划 主机名 角色 iphdss7-21.host.com kubelet 10.4.7.21hdss7-22.host.com kubelet 10.4.7.22注意:这里部署文档以hdds7-21.host.com主机为例,另外一台运算节点安装部署方法类似 7.1.1、签发kubelet证书运维主机hdss7-200.host.com上: 1.创建生成证书签名请求(csr)的json配置文件 $ vim /opt/certs/kubelet-csr.json{ "CN": "k8s-kubelet", "hosts": [ "127.0.0.1", "10.4.7.10", "10.4.7.21", "10.4.7.22", "10.4.7.23", "10.4.7.24", "10.4.7.25", "10.4.7.26", "10.4.7.27", "10.4.7.28" "key": { "algo": "rsa", "size": 2048 "names": [ "C": "CN", "ST": "beijing", "L": "beijing", "O": "od", "OU": "ops" }2.生成证书和私钥 $ cd /opt/certs$ cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server kubelet-csr.json | cfssl-json -bare kubelet2020/01/10 20:15:39 [INFO] generate received request2020/01/10 20:15:39 [INFO] received CSR2020/01/10 20:15:39 [INFO] generating key: rsa-20482020/01/10 20:15:40 [INFO] encoded CSR2020/01/10 20:15:40 [INFO] signed certificate with serial number 5262511356647668150561792065118449932082576852502020/01/10 20:15:40 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for For more information see the Baseline Requirements for the Issuance and Management Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org); specifically, section 10.2.3 ("Information Requirements").3.检查证书和私钥 $ ll kubelet*-rw-r--r-- 1 root root 452 Jan 10 20:15 kubelet-csr.json-rw------- 1 root root 1675 Jan 10 20:15 kubelet-key.pem-rw-r--r-- 1 root root 1115 Jan 10 20:15 kubelet.csr-rw-r--r-- 1 root root 1468 Jan 10 20:15 kubelet.pem7.1.2、kubelet配置hdss7-21.host.com上: 1.拷贝证书到各运算节点,并创建配置(证书、私钥,注意私钥文件权限600) $ ll /opt/kubernetes/server/bin/cert/total 32-rw------- 1 root root 1679 Jan 10 16:32 apiserver-key.pem-rw-r--r-- 1 root root 1598 Jan 10 16:32 apiserver.pem-rw------- 1 root root 1675 Jan 10 16:32 ca-key.pem-rw-r--r-- 1 root root 1346 Jan 10 16:32 ca.pem-rw------- 1 root root 1679 Jan 10 16:32 client-key.pem-rw-r--r-- 1 root root 1363 Jan 10 16:32 client.pem-rw------- 1 root root 1675 Jan 10 20:20 kubelet-key.pem-rw-r--r-- 1 root root 1468 Jan 10 20:20 kubelet.pem2.创建kubelet配置文件 基于https的方式访问到nginx反代的vip 进入指定目录 $ cd /opt/kubernetes/server/bin/conf/ 指定根证书和api-server的vip $ kubectl config set-cluster myk8s \ --certificate-authority=/opt/kubernetes/server/bin/cert/ca.pem \ --embed-certs=true \ --server=https://10.4.7.10:7443 \ --kubeconfig=kubelet.kubeconfig 拿客户端密钥和api-server通信 $ kubectl config set-credentials k8s-node \ --client-certificate=/opt/kubernetes/server/bin/cert/client.pem \ --client-key=/opt/kubernetes/server/bin/cert/client-key.pem \ --embed-certs=true \ --kubeconfig=kubelet.kubeconfig 以k8s-node用户去访问api-server(该用户需要授权) $ kubectl config set-context myk8s-context \ --cluster=myk8s \ --user=k8s-node \ --kubeconfig=kubelet.kubeconfig $ kubectl config use-context myk8s-context --kubeconfig=kubelet.kubeconfig关于kubeconfig文件这是一个k8s用户的配置文件它里面含有证书信息证书过期或更换,需要同步替换该文件3.创建授权资源配置文件k8s-node.yaml 创建一次即可,用于给k8s-node这个访问账户授权,权限为k8s节点 $ vim /opt/kubernetes/server/bin/conf/k8s-node.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: k8s-noderoleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: system:nodesubjects: apiGroup: rbac.authorization.k8s.io kind: User name: k8s-nodeUser account是为人设计的,而service account则是为Pod中的进程调用Kubernetes API而设计;User account是跨namespace的,而service account则是仅局限它所在的namespace4.使用kubctl创建 $ kubectl create -f /opt/kubernetes/server/bin/conf/k8s-node.yaml7.1.3、准备pause基础镜像pause镜像是k8s里必不可少的以pod方式运行业务容器时的一个基础容器。 运维主机hdss7-200.host.com上: $ docker pull kubernetes/pause2.提交至私有仓库(harbor)中 $ docker tag f9d5de079539 harbor.od.com/public/pause:latest$ docker push harbor.od.com/public/pause:latest7.1.4、创建kubelet启动脚本hdss7-21.host.com上: 1.启动脚本 $ vim /opt/kubernetes/server/bin/kubelet.sh !/bin/sh ./kubelet \ --anonymous-auth=false \ --cgroup-driver systemd \ --cluster-dns 192.168.0.2 \ --cluster-domain cluster.local \ --runtime-cgroups=/systemd/system.slice \ --kubelet-cgroups=/systemd/system.slice \ --fail-swap-on="false" \ --client-ca-file ./cert/ca.pem \ --tls-cert-file ./cert/kubelet.pem \ --tls-private-key-file ./cert/kubelet-key.pem \ --hostname-override hdss7-22.host.com \ --image-gc-high-threshold 20 \ --image-gc-low-threshold 10 \ --kubeconfig ./conf/kubelet.kubeconfig \ --log-dir /data/logs/kubernetes/kube-kubelet \ --pod-infra-container-image harbor.od.com/public/pause:latest \ --root-dir /data/kubeletcluster-dns:指定集群内部dns地址 hostname-override:当前机器主机名 pod-infra-container-image:pause镜像拉取地址 kubeconfig:指定上面创建的上下文配置文件 参数配置解析 参数 说明--anonymous-auth 允许匿名请求到 kubelet 服务。未被另一个身份验证方法拒绝的请求被视为匿名请求。匿名请求包含系统的用户名: anonymous ,以及系统的组名: unauthenticated (默认 true )--cgroup-driver 可选值有cgroupfs和systemd(默认cgroupfs)与docker驱动一致--cluster-dns DNS 服务器的IP列表,多个用逗号分隔--cluster-domain 集群域名, kubelet 将配置所有容器除了主机搜索域还将搜索当前域--fail-swap-on 如果设置为true则启动kubelet失败(default true)--hostname-override cluster中的node name--image-gc-high-threshold 磁盘使用率最大值,超过此值将执行镜像垃圾回收(default 85)--image-gc-low-threshold 磁盘使用率最大值,低于此值将停止镜像垃圾回收(default 80)--kubeconfig 用来指定如何连接到 API server--pod-infra-container-image 每个 pod 中的 network/ipc 命名空间容器将使用的pause镜像--root-dir kubelet 的工作目录创建目录 $ mkdir -p /data/logs/kubernetes/kube-kubelet /data/kubelet给脚本添加+x权限 $ chmod +x /opt/kubernetes/server/bin/kubelet.sh2.创建kubelet的启动配置 $ vi /etc/supervisord.d/kube-kubelet.ini[program:kube-kubelet-7-21]command=/opt/kubernetes/server/bin/kubelet.sh ; the program (relative uses PATH, can take args)numprocs=1 ; number of processes copies to start (def 1)directory=/opt/kubernetes/server/bin ; directory to cwd to before exec (def no cwd)autostart=true ; start at supervisord start (default: true)autorestart=true ; retstart at unexpected quit (default: true)startsecs=30 ; number of secs prog must stay running (def. 1)startretries=3 ; max # of serial start failures (default 3)exitcodes=0,2 ; 'expected' exit codes for process (default 0,2)stopsignal=QUIT ; signal used to kill process (default TERM)stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)user=root ; setuid to this UNIX account to run the programredirect_stderr=true ; redirect proc stderr to stdout (default false)stdout_logfile=/data/logs/kubernetes/kube-kubelet/kubelet.stdout.log ; stderr log path, NONE for none; default AUTOstdout_logfile_maxbytes=64MB ; max # logfile bytes b4 rotation (default 50MB)stdout_logfile_backups=4 ; # of stdout logfile backups (default 10)stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)stdout_events_enabled=false ; emit events on stdout writes (default false)killasgroup=truestopasgroup=true3.启动并检查 $ supervisorctl update$ supervisorctl statusetcd-server-7-21 RUNNING pid 4148, uptime 5:20:56kube-apiserver-7-21 RUNNING pid 4544, uptime 4:15:45kube-controller-manager-7-21 RUNNING pid 4690, uptime 3:13:41kube-kubelet-7-21 RUNNING pid 5099, uptime 0:01:33kube-scheduler-7-21 RUNNING pid 4727, uptime 3:10:164.查看节点 给节点打上标签 $ kubectl label node hdss7-22.host.com node-role.kubernetes.io/node=$ kubectl label node hdss7-22.host.com node-role.kubernetes.io/master= $ kubectl get nodesNAME STATUS ROLES AGE VERSIONhdss7-21.host.com Ready master,node 9m56s v1.15.2hdss7-22.host.com Ready master,node 4m5s v1.15.27.2、部署kube-proxy集群规划 主机名 角色 iphdss7-21.host.com kubelet 10.4.7.21hdss7-22.host.com kube-proxy 10.4.7.22注意:这里部署文档以hdds7-21.host.com主机为例,另外一台运算节点安装部署方法类似 7.2.1、签发kube-proxy证书运维主机hdss-200.host.com上: 1.创建生成证书签名请求(csr)的json配置文件 $ vim /opt/certs/kube-proxy-csr.json{ "CN": "system:kube-proxy", "key": { "algo": "rsa", "size": 2048 "names": [ "C": "CN", "ST": "beijing", "L": "beijing", "O": "od", "OU": "ops" }2.生成证书和私钥 $ cd /opt/certs$ cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client kube-proxy-csr.json |cfssl-json -bare kube-proxy-client2020/01/10 21:12:40 [INFO] generate received request2020/01/10 21:12:40 [INFO] received CSR2020/01/10 21:12:40 [INFO] generating key: rsa-20482020/01/10 21:12:41 [INFO] encoded CSR2020/01/10 21:12:41 [INFO] signed certificate with serial number 3778576445530480661954559489358223754015007926122020/01/10 21:12:41 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for For more information see the Baseline Requirements for the Issuance and Management Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org); specifically, section 10.2.3 ("Information Requirements").3.检查证书和私钥 $ ll kube-proxy*-rw------- 1 root root 1675 Jan 10 21:12 kube-proxy-client-key.pem-rw-r--r-- 1 root root 1005 Jan 10 21:12 kube-proxy-client.csr-rw-r--r-- 1 root root 1375 Jan 10 21:12 kube-proxy-client.pem-rw-r--r-- 1 root root 267 Jan 10 21:12 kube-proxy-csr.json7.2.2、Kube-proxy配置hdss7-21.host.com上: 1.拷贝证书到各运算节点,并创建配置(证书、私钥,注意私钥文件权限600) $ lltotal 40-rw------- 1 root root 1679 Jan 10 16:32 apiserver-key.pem-rw-r--r-- 1 root root 1598 Jan 10 16:32 apiserver.pem-rw------- 1 root root 1675 Jan 10 16:32 ca-key.pem-rw-r--r-- 1 root root 1346 Jan 10 16:32 ca.pem-rw------- 1 root root 1679 Jan 10 16:32 client-key.pem-rw-r--r-- 1 root root 1363 Jan 10 16:32 client.pem-rw------- 1 root root 1675 Jan 10 21:16 kube-proxy-client-key.pem-rw-r--r-- 1 root root 1375 Jan 10 21:16 kube-proxy-client.pem-rw------- 1 root root 1675 Jan 10 20:20 kubelet-key.pem-rw-r--r-- 1 root root 1468 Jan 10 20:20 kubelet.pem2.创建kube-proxy配置 进入指定目录 $ cd /opt/kubernetes/server/bin/conf/ $ kubectl config set-cluster myk8s \ --certificate-authority=/opt/kubernetes/server/bin/cert/ca.pem \ --embed-certs=true \ --server=https://10.4.7.10:7443 \ --kubeconfig=kube-proxy.kubeconfig $ kubectl config set-credentials kube-proxy \ --client-certificate=/opt/kubernetes/server/bin/cert/kube-proxy-client.pem \ --client-key=/opt/kubernetes/server/bin/cert/kube-proxy-client-key.pem \ --embed-certs=true \ --kubeconfig=kube-proxy.kubeconfig $ kubectl config set-context myk8s-context \ --cluster=myk8s \ --user=kube-proxy \ --kubeconfig=kube-proxy.kubeconfig $ kubectl config use-context myk8s-context --kubeconfig=kube-proxy.kubeconfig7.2.3、创建kube-proxy启动脚本hdss7-21.host.com上: 1.加载ipvs模块 $ vi /root/ipvs.sh !/bin/bash ipvs_mods_dir="/usr/lib/modules/$(uname -r)/kernel/net/netfilter/ipvs"for i in $(ls $ipvs_mods_dir|grep -o "^1*")do /sbin/modinfo -F filename $i &amp;&gt;/dev/null if [ $? -eq 0 ];then /sbin/modprobe $i fidone添加+x权限 $ chmod +x /root/ipvs.sh执行脚本并检查ip_vs模块是否加载 $ sh /root/ipvs.sh$ lsmod | grep ip_vs2.创建启动脚本 $ vim /opt/kubernetes/server/bin/kube-proxy.sh !/bin/sh ./kube-proxy \ --cluster-cidr 172.7.0.0/16 \ --hostname-override hdss7-21.host.com \ --proxy-mode=ipvs \ --ipvs-scheduler=nq \ --kubeconfig ./conf/kube-proxy.kubeconfigcluster-cidr:指定docker ip范围 创建日志存放目录 $ mkdir -p /data/logs/kubernetes/kube-proxy给脚本添加+x权限 $ chmod +x /opt/kubernetes/server/bin/kube-proxy.sh2.创建kubelet的启动配置 $ vi /etc/supervisord.d/kube-proxy.ini[program:kube-proxy-7-21]command=/opt/kubernetes/server/bin/kube-proxy.sh ; the program (relative uses PATH, can take args)numprocs=1 ; number of processes copies to start (def 1)directory=/opt/kubernetes/server/bin ; directory to cwd to before exec (def no cwd)autostart=true ; start at supervisord start (default: true)autorestart=true ; retstart at unexpected quit (default: true)startsecs=30 ; number of secs prog must stay running (def. 1)startretries=3 ; max # of serial start failures (default 3)exitcodes=0,2 ; 'expected' exit codes for process (default 0,2)stopsignal=QUIT ; signal used to kill process (default TERM)stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)user=root ; setuid to this UNIX account to run the programredirect_stderr=true ; redirect proc stderr to stdout (default false)stdout_logfile=/data/logs/kubernetes/kube-proxy/proxy.stdout.log ; stderr log path, NONE for none; default AUTOstdout_logfile_maxbytes=64MB ; max # logfile bytes b4 rotation (default 50MB)stdout_logfile_backups=4 ; # of stdout logfile backups (default 10)stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)stdout_events_enabled=false ; emit events on stdout writes (default false)killasgroup=truestopasgroup=true启动并检查 $ supervisorctl update$ supervisorctl statusetcd-server-7-21 RUNNING pid 4148, uptime 5:59:11kube-apiserver-7-21 RUNNING pid 4544, uptime 4:54:00kube-controller-manager-7-21 RUNNING pid 4690, uptime 3:51:56kube-kubelet-7-21 RUNNING pid 5099, uptime 0:39:48kube-proxy-7-21 RUNNING pid 14452, uptime 0:00:45kube-scheduler-7-21 RUNNING pid 4727, uptime 3:48:313.安装ipvs管理工具 $ yum install ipvsadm -y4.检查ipvs是否成功 $ ipvsadm -LnIP Virtual Server version 1.2.1 (size=4096)Prot LocalAddress:Port Scheduler Flags -&gt; RemoteAddress:Port Forward Weight ActiveConn InActConnTCP 192.168.0.1:443 nq -&gt; 10.4.7.21:6443 Masq 1 0 0 -&gt; 10.4.7.22:6443 Masq 1 0 08、验证集群1.在任意一个运算节点, 创建一个资源清单 这里我们选hdss7-21.host.com主机 $ vi /root/nginx-ds.yamlapiVersion: extensions/v1beta1kind: DaemonSetmetadata: name: nginx-dsspec: template: metadata: labels: app: nginx-ds spec: containers: - name: my-nginx image: harbor.od.com/public/nginx:v1.7.9 ports: - containerPort: 80 $ kubectl create -f /root/nginx-ds.yamldaemonset.extensions/nginx-ds created查看pod状态 $ kubctl get podsNAME READY STATUS RESTARTS AGEnginx-ds-gl9mg 1/1 Running 0 20snginx-ds-mlptx 1/1 Running 0 20s2.查看集群状态 $ kubectl get csNAME STATUS MESSAGE ERRORcontroller-manager Healthy okscheduler Healthy oketcd-1 Healthy {"health": "true"}etcd-0 Healthy {"health": "true"}etcd-2 Healthy {"health": "true"} $ kubectl get nodeNAME STATUS ROLES AGE VERSIONhdss7-21.host.com Ready master,node 48m v1.15.2hdss7-22.host.com Ready master,node 42m v1.15.2 原文地址https://www.cnblogs.com/jasonminghao/p/12716239.html

震撼!全网第一张源码分析全景图揭秘Nginx

震撼!全网第一张源码分析全景图揭秘Nginx 不管是C/C++技术栈,还是PHP,Java技术栈,从事后端开发的朋友对nginx一定不会陌生。 想要深入学习nginx,阅读源码一定是非常重要的一环,但nginx源码量毕竟还是不算少,一不小心就容易陷入某个细节,迷失在茫茫码海之中。 如果有一张地图,让我们开启上帝视角,总览全局,帮助我们快速学习整体框架结构,又能不至于迷失其中那就再好不过了! 看到这篇文章的你有福了,笔者花了不少时间,把这件事给做了,先来看个全貌(限于平台图片尺寸设定,这里只能看个大概,想获取高清大图请看文末): 下面选取一些关键部分来一窥神秘的nginx。 主进程启动nginx主进程启动后,进行一系列的初始化,包括但不限于: 命令行参数解析时间初始化日志初始化ssl初始化操作系统相关初始化一致性hash表初始化模块编号处理 核心初始化另外一个最重要的初始化由ngx_init_cycle()函数完成,该函数围绕nginx中非常核心的一个全局数据结构ngx_cycle_t展开。 该函数完成了几个核心初始化: 配置文件解析创建并监听socket初始化nginx各模块 nginx核心模块群nginx是一个模块化设计的软件,优秀的架构设计使得nginx可以扩展非常多的模块。 要一一描绘出这些模块显得有些杂乱和工作量巨大,仅选取一些关键核心模块进行了展示: 每个模块有一个支持的命令解析列表,在初始化过程中,主进程将会遍历所有模块的命令列表,进行配置文件中的命令解析,如经常用的ngx_http_proxy_module: ngx_http_core_module模块: main函数的最后,根据是否启用多进程模型,分别进入多进程版本的ngx_master_process_cycle和单进程版本的ngx_single_process_cycle()。 以常见的多进程版本为例,进入该函数后,首先设置进程名称为:"master process",随后启动各工作子进程。 启动子进程经过几层封装,最终通过fork启动多个子进程: 除了工作子进程,还启动了缓存管理进程。 之后主进程进入工作循环,周期性更新时间并检查各全局标记,根据不同情况给子进程发送不同信号。 子进程工作循环子进程启动后,进入ngx_worker_process_cycle,进行一些工作进程的初始化,随后修改进程名称为:"worker process"。 接着进入工作循环函数ngx_process_events_and_timers,在该函数中主要负责: 竞争互斥锁,拿到锁的进程才能执行accept接受新的连接,以此在多进程之间解决惊群效应通过epoll异步IO模型处理网络IO事件,包括新的连接事件和已建立连接发生的读写事件处理定时器队列中到期的定时器事件,定时器通过红黑树的方式存储 HTTP请求预处理当连接有数据产生时,工作线程读取socket中到来的数据,并根据HTTP协议格式进行解析,最终封装成ngx_request_t请求对象,提交处理。 HTTP请求处理的11个阶段在nginx中各HTTP模块是以挂载的形式串接而成,以流水线工作模式进行HTTP请求的处理,nginx将一个HTTP请求的处理划分为11个阶段。 typedef enum { NGX_HTTP_POST_READ_PHASE = 0, NGX_HTTP_SERVER_REWRITE_PHASE, NGX_HTTP_FIND_CONFIG_PHASE, NGX_HTTP_REWRITE_PHASE, NGX_HTTP_POST_REWRITE_PHASE, NGX_HTTP_PREACCESS_PHASE, NGX_HTTP_ACCESS_PHASE, NGX_HTTP_POST_ACCESS_PHASE, NGX_HTTP_PRECONTENT_PHASE, NGX_HTTP_CONTENT_PHASE, NGX_HTTP_LOG_PHASE } ngx_http_phases;每阶段(部分阶段保留,不允许挂载)允许多个模块挂载,一个模块也可以挂载到多个阶段。因此,初次完成挂载的存储结构是一个二维数组的形式。 不过在初始化过程中,ngx_http_init_phase_handlers函数将该二维数组转换成了一维数组。下图是nginx中各模块挂载情况: 全景图最后,再来看一看全貌: 总结nginx不仅是一款优秀的高性能web服务器,对于C/C++技术栈的同学来说,还是一个很好的学习对象,其良好的架构设计,优美的代码风格和经典的编程技法无一不值得细细品来。 不过限于笔者水平和时间有限,虽然号称全景图,但依然无法覆盖到nginx的方方面面,欢迎读者朋友留言交流,让此图日渐完善,谢谢大家。 获取完整高清大图,可在公众号里回复“nginx”自动获取。 原文地址https://www.cnblogs.com/xuanyuan/p/12710715.html

Docker 常用命令(.NET Core示例)

Docker 常用命令(.NET Core示例) 本篇文章,整理docker中常用的命令,方便大家学习和命令查询。最后分享一个.NET Core docker部署的示例。 Docker安装CentOS Docker 安装安装 Docker Desktop for Mac、Docker Desktop for Windows设置docker仓库镜像加速器迁移Docker默认存储目录 Docker运行基本命令docker [--helper]:显示所有docker命令docker [command] --help:显示指定命令的帮助文档docker info:显示docker系统信息docker version:显示docker版本信息docker stats:显示运行的容器占用的容器资源(eg:容器名、cpu、内存、io等)(Ctrl+C退出)systemctl status docker:显示docker的运行状态systemctl start docker:启动dockersystemctl stop docker:关闭dockersystemctl restart docker:重启docker Docker仓库Docker Hubdocker login -u 用户名 -p 密码 [仓库地址]:登陆到一个Docker镜像仓库,如果未指定镜像仓库地址,默认为官方仓库 Docker Hubdocker logout:推出仓库docker search [image id or name]:从Docker Hub查找镜像docker pull [image id or name]:拉取镜像docker tag [local image id or name]:[tag] [registry host]/[镜像仓库]/image name: 标记本地镜像,将其归入某一仓库。docker push [image id or name]:推送镜像到Docker Hub 、示例:推送到阿里云images仓库 sudo docker login --username=* registry.cn-shenzhen.aliyuncs.comsudo docker tag [ImageId] registry.cn-shenzhen.aliyuncs.com/mk-application/mk.admin:[镜像版本号]sudo docker push registry.cn-shenzhen.aliyuncs.com/mk-application/mk.admin:[镜像版本号](registry.cn-shenzhen.aliyuncs.com/mk-application/mk.admin 为images仓库地址) images 操作docker tag [image id] REPOSITORY:TAG(仓库:标签):重命名镜像名docker images [options]:显示所有镜像文件常用OPTIONS说明:-a :列出本地所有的镜像-f :显示满足条件的镜像;-q :只显示镜像IDdocker image inspect : 获取镜像的元数据。docker build [options] [image id or name] .:构建一个镜像。 (注意:最后空格后面有一个.)。常用OPTIONS说明:--tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;-f :指定要使用的Dockerfile路径;(默认当前目录的Dockerfile)-m :设置内存最大值;docker rmi [-f] [image id or name]:删除指定镜像(-f :强制删除)docker rmi $(docker images -q):删除所有镜像docker commit [container id or name] [image id or name[:tag]]:从容器创建一个新的镜像。docker images | grep "redis" :查询所有包含redis的镜像 注意:REPOSITORY 列,表示:镜像名称,用于标识镜像 、镜像归档 将指定镜像保存成 tar 归档文件docker save -o /root/**.tar [image id or name]:docker save [image id or name]&gt;/root/**.tar导入使用 docker save 命令导出的镜像docker load -i /root/**.tardocker load/**.tar从归档文件中创建镜像docker import /root/**.tar [image id or name]:从镜像归档文件创建指定命名的镜像 container 操作docker ps:查看当前运行的容器docker ps [OPTIONS]常用OPTIONS说明:-a:显示所有的容器--filter,-f:根据条件过滤显示的内容(eg:-f name=imc.user,过滤容器名字为"imc.user")-n:列出最近创建的n个容器docker container inspect : 获取容器的元数据。docker rename [container id or name] [new Name]:重命名容器名docker run [OPTIONS] [image id or name]:创建一个新的容器并运行常用OPTIONS说明:-d: 后台运行容器,并返回容器ID;-p(小写): 指定端口映射,格式为:主机(宿主)端口:容器端口-name: 为容器指定一个名称;-m :设置容器使用内存最大值;--volume , -v: 绑定一个卷--restart=always:总是重启容器。(Docker容器的重启策略及docker run的--restart选项详解)docker update [options] [container id or name]:更新容器配置常用options说明-m:内存限制--restart=no|always|on-failure:重启策略docker stop [container id or name]:停止运行指定容器docker stop $(docker ps -q -f status=running):停掉所有正在运行的容器docker start [container id or name]:开启指定容器docker restart [container id or name]:重启指定容器docker rm [-f] [container id or name]:删除指定容器,加-f参数强制删除docker rm $(docker ps -a -q):删除所有停止的容器 、查看容器内部信息 docker exec -it [container id or name] bash:在运行的容器中执行bash命令,比如执行ls命令列出目录或者查看文件。(退出容器:Ctrl + D 或 exit)docker top [options] [container id or name]:查看容器中运行的进程信息,支持 ps 命令参数。(能否用exec命令代替?不能,因为容器运行时不一定有/bin/bash终端来交互执行top命令,而且容器还不一定有top命令)docker pause [container id or name]:暂停容器中所有的进程docker unpause [container id or name]:恢复容器中所有的进程docker diff [container id or name]:检查容器里文件结构的更改。(相对原始镜像的文件结构) 、查看容器运行日志 docker启动后默认日志位置:/var/lib/docker/containers/容器ID/容器ID-json.logdocker logs [options] [container id or name]:查看指定容器Id的的运行日志OPTIONS说明:-f : 跟踪日志输出--since :显示某个开始时间的所有日志-t : 显示时间戳--tail :仅列出最新N条容器日志示例:查看容器mynginx从2016年7月1日后的最新10条日志。docker logs --since="2016-07-01" --tail=10 mynginx 、容器与主机之间的数据拷贝 docker cp:用于容器与主机之间的数据拷贝。docker cp [container id or name]:src_path dest_pathdocker cp src_path [container id or name]:dest_path volume(数据卷)Docker Image可以理解成多个只读文件叠加而成,因此Docker Image是只读的。当我们将其运行起来,就相当于在只读的Image外包裹了一层读写层变成了容器。当你删除容器之后,使用这个镜像重新创建一个容器,此时的镜像的只读层还和原来的一样,但是你在读写层的修改全部都会丢失。docker使用volume实现数据的持久化,实现容器和容器之间,容器和host之间共享数据。volume的大小不会被加到容器本身上。 命令docker volume create --name [volume name]:创建命名的volume,创建目录默认:/var/lib/docker/volumes/[volume name]/_data/docker volume ls:查看当前所有volumedocker volume inspect [volume name]:查看volume详细信息docker volume rm [volume name]:删除volume(没有被容器使用的volume才能被删除) docker run .... -v 宿主目录文件:容器目录文件 :建立目录或文件的映射docker run .... -v [volume name]:容器目录docker run .... -v 容器目录 :[自管理卷模式]docker自动创建匿名的volume。默认目录/var/lib/docker/volumes/[container id]/_data。(自管理卷的volume删除方式:删除容器时,加 -v)-v 参数的注意事项: 、host机器的目录路径必须为全路径(准确的说需要以/或~/开始的路径) 、如果host机器上的目录不存在,docker会自动创建该目录 、如果container中的目录不存在,docker会自动创建该目录 、如果container中的目录已经有内容,那么docker会使用host上的目录将其覆盖掉 、linux下 $PWD 是一个系统环境变量,指代当前目录环境 、windows下的路径如:D:PycharmProjects 要写为 /d/PycharmProjects (试试原始目录行不信) 示例:docker运行一个 .Net Core 程序目录规划:------container // 容器根目录--------mk.admin // 容器名----------Logs // 用于映射日志的目录----------Config // 用于映射配置的目录 .NET Core端口设置host.json文件{"urls": "http://*:44380"} 第一步:发布.net core应用程序。在要发布的项目目录执行如下命令:(或使用vs工具执行发布)dotnet publish -f netcoreapp3.1 -o **publishmk.admin 第二步:创建 Dockerfile 定制镜像找 dotnet core 需要的运行时:docker image:ASP.NET Core Runtime将Dockerfile放在发布目录下(**publishmk.admin)内容:0102030405060708091011121314 第一个指令,FROM 指定基础构建镜像 基于 microsoft/aspnet:3.1 来构建我们的镜像 FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 拷贝项目publish文件夹中的所有文件到 docker容器中的publish文件夹中  COPY . /publish 设置工作目录为 /publish 文件夹,即容器启动默认的文件夹 WORKDIR /publish EXPOSE 44380 使用dotnet ***.dll来运行应用程序 ENTRYPOINT ["dotnet", "Mk.Admin.HttpApi.Host.dll"] 第三步:创建镜像在发布目录 **publishmk.admin 目录下,执行命令:01020304050607 、构建镜像 docker build -t img.mk.admin . 、运行容器 docker run --name mk.admin -p 44380:44380 -v /usr/application/Mk.Admin/Logs:/publish/Logs -d --restart=always img.mk.admin(数据卷:将宿主机的/usr/application/Mk.Admin/Logs目录映射到容器的/publish/Logs目录,这样就可以在宿主机上查看和删除日志文件) 、查看运行中的容器 docker ps 、输入地址,查看站点运行状态 原文地址https://www.cnblogs.com/heyuquan/p/docker-cmd-and-dotnetcore.html

在 Spring Boot 中使用 Dataway 配置数据查询接口

在 Spring Boot 中使用 Dataway 配置数据查询接口 Dataway介绍Dataway 是基于 DataQL 服务聚合能力,为应用提供的一个接口配置工具。使得使用者无需开发任何代码就配置一个满足需求的接口。 整个接口配置、测试、冒烟、发布。一站式都通过 Dataway 提供的 UI 界面完成。UI 会以 Jar 包方式提供并集成到应用中并和应用共享同一个 http 端口,应用无需单独为 Dataway 开辟新的管理端口。 这种内嵌集成方式模式的优点是,可以使得大部分老项目都可以在无侵入的情况下直接应用 Dataway。进而改进老项目的迭代效率,大大减少企业项目研发成本。 Dataway 工具化的提供 DataQL 配置能力。这种研发模式的变革使得,相当多的需求开发场景只需要配置即可完成交付。 从而避免了从数据存取到前端接口之间的一系列开发任务,例如:Mapper、BO、VO、DO、DAO、Service、Controller 统统不在需要。 Dataway 是 Hasor 生态中的一员,因此在  Spring 中使用 Dataway 首先要做的就是打通两个生态。根据官方文档中推荐的方式我们将 Hasor 和 Spring Boot 整合起来。这里是原文:https://www.hasor.net/web/extends/spring/for_boot.html 第一步:引入相关依赖 &lt;groupId&gt;net.hasor&lt;/groupId&gt; &lt;artifactId&gt;hasor-spring&lt;/artifactId&gt; &lt;version&gt;4.1.3&lt;/version&gt; &lt;groupId&gt;net.hasor&lt;/groupId&gt; &lt;artifactId&gt;hasor-dataway&lt;/artifactId&gt; &lt;version&gt;4.1.3-fix20200414&lt;/version&gt;&lt;!-- 4.1.3 包存在UI资源缺失问题 --&gt; hasor-spring 负责 Spring 和 Hasor 框架之间的整合。hasor-dataway 是工作在 Hasor 之上,利用 hasor-spring 我们就可以使用 dataway了。 第二步:配置 Dataway,并初始化数据表dataway 会提供一个界面让我们配置接口,这一点类似 Swagger 只要jar包集成就可以实现接口配置。找到我们 springboot 项目的配置文件 application.properties 是否启用 Dataway 功能(必选:默认false) HASOR_DATAQL_DATAWAY=true 是否开启 Dataway 后台管理界面(必选:默认false) HASOR_DATAQL_DATAWAY_ADMIN=true dataway API工作路径(可选,默认:/api/) HASOR_DATAQL_DATAWAY_API_URL=/api/ dataway-ui 的工作路径(可选,默认:/interface-ui/) HASOR_DATAQL_DATAWAY_UI_URL=/interface-ui/ SQL执行器方言设置(可选,建议设置) HASOR_DATAQL_FX_PAGE_DIALECT=mysqlDataway 一共涉及到 5个可以配置的配置项,但不是所有配置都是必须的。 其中 HASOR_DATAQL_DATAWAY、HASOR_DATAQL_DATAWAY_ADMIN 两个配置是必须要打开的,默认情况下 Datawaty 是不启用的。 Dataway 需要两个数据表才能工作,下面是这两个数据表的简表语句。下面这个 SQL 可以在 dataway的依赖 jar 包中 “META-INF/hasor-framework/mysql” 目录下面找到,建表语句是用 mysql 语法写的。 CREATE TABLE interface_info ( `api_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID', `api_method` varchar(12) NOT NULL COMMENT 'HttpMethod:GET、PUT、POST', `api_path` varchar(512) NOT NULL COMMENT '拦截路径', `api_status` int(2) NOT NULL COMMENT '状态:0草稿,1发布,2有变更,3禁用', `api_comment` varchar(255) NULL COMMENT '注释', `api_type` varchar(24) NOT NULL COMMENT '脚本类型:SQL、DataQL', `api_script` mediumtext NOT NULL COMMENT '查询脚本:xxxxxxx', `api_schema` mediumtext NULL COMMENT '接口的请求/响应数据结构', `api_sample` mediumtext NULL COMMENT '请求/响应/请求头样本数据', `api_create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `api_gmt_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`api_id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='Dataway 中的API'; CREATE TABLE interface_release ( `pub_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Publish ID', `pub_api_id` int(11) NOT NULL COMMENT '所属API ID', `pub_method` varchar(12) NOT NULL COMMENT 'HttpMethod:GET、PUT、POST', `pub_path` varchar(512) NOT NULL COMMENT '拦截路径', `pub_status` int(2) NOT NULL COMMENT '状态:0有效,1无效(可能被下线)', `pub_type` varchar(24) NOT NULL COMMENT '脚本类型:SQL、DataQL', `pub_script` mediumtext NOT NULL COMMENT '查询脚本:xxxxxxx', `pub_script_ori` mediumtext NOT NULL COMMENT '原始查询脚本,仅当类型为SQL时不同', `pub_schema` mediumtext NULL COMMENT '接口的请求/响应数据结构', `pub_sample` mediumtext NULL COMMENT '请求/响应/请求头样本数据', `pub_release_time`datetime DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间(下线不更新)', PRIMARY KEY (`pub_id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='Dataway API 发布历史。'; create index idx_interface_release on interface_release (pub_api_id); 第三步:配置数据源作为 Spring Boot 项目有着自己完善的数据库方面工具支持。我们这次采用 druid + mysql + spring-boot-starter-jdbc 的方式。 首先引入依赖 &lt;groupId&gt;mysql&lt;/groupId&gt; &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt; &lt;version&gt;5.1.30&lt;/version&gt; &lt;groupId&gt;com.alibaba&lt;/groupId&gt; &lt;artifactId&gt;druid&lt;/artifactId&gt; &lt;version&gt;1.1.21&lt;/version&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-jdbc&lt;/artifactId&gt; &lt;groupId&gt;com.alibaba&lt;/groupId&gt; &lt;artifactId&gt;druid-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.1.10&lt;/version&gt; 然后增加数据源的配置 spring.datasource.url=jdbc:mysql://xxxxxxx:3306/examplespring.datasource.username=xxxxxspring.datasource.password=xxxxxspring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.type:com.alibaba.druid.pool.DruidDataSource druid spring.datasource.druid.initial-size=3spring.datasource.druid.min-idle=3spring.datasource.druid.max-active=10spring.datasource.druid.max-wait=60000spring.datasource.druid.stat-view-servlet.login-username=adminspring.datasource.druid.stat-view-servlet.login-password=adminspring.datasource.druid.filter.stat.log-slow-sql=truespring.datasource.druid.filter.stat.slow-sql-millis=1如果项目已经集成了自己的数据源,那么可以忽略第三步。 第四步:把数据源设置到 Hasor 容器中Spring Boot 和 Hasor 本是两个独立的容器框架,我们做整合之后为了使用 Dataway 的能力需要把 Spring 中的数据源设置到 Hasor 中。 首先新建一个 Hasor 的 模块,并且将其交给 Spring 管理。然后把数据源通过 Spring 注入进来。 @DimModule@Componentpublic class ExampleModule implements SpringModule { @Autowired private DataSource dataSource = null; @Override public void loadModule(ApiBinder apiBinder) throws Throwable { // .DataSource form Spring boot into Hasor apiBinder.installModule(new JdbcModule(Level.Full, this.dataSource)); }Hasor 启动的时候会调用 loadModule 方法,在这里再把 DataSource 设置到 Hasor 中。 第五步:在SprintBoot 中启用 Hasor@EnableHasor()@EnableHasorWeb()@SpringBootApplication(scanBasePackages = { "net.example.hasor" })public class ExampleApplication { public static void main(String[] args) { SpringApplication.run(ExampleApplication.class, args); }这一步非常简单,只需要在 Spring 启动类上增加两个注解即可。 第六步:启动应用应用在启动过程中会看到 Hasor Boot 的欢迎信息 ____ _| | | | | _ | || |__| | | |_) | | |_| _ |/ _` / __|/ | '__| | &lt; / / _ | __|| | | | (_| _ (_) | | | |_) | (_) | (_) | ||_| |_|__,_|___/___/|_| |____/ ___/ ___/ __|在后面的日志中还可以看到类似下面这些日志。 2020-04-14 13:52:59.696 [main] INFO n.h.core.context.TemplateAppContext - loadModule class net.hasor.dataway.config.DatawayModule2020-04-14 13:52:59.697 [main] INFO n.hasor.dataway.config.DatawayModule - dataway api workAt /api/2020-04-14 13:52:59.697 [main] INFO n.h.c.e.AbstractEnvironment - var -&gt; HASOR_DATAQL_DATAWAY_API_URL = /api/.2020-04-14 13:52:59.704 [main] INFO n.hasor.dataway.config.DatawayModule - dataway admin workAt /interface-ui/2020-04-14 13:52:59.716 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[901d38f22faa419a8593bb349905ed0e] -&gt; bindType ‘class net.hasor.dataway.web.ApiDetailController’ mappingTo: ‘[/interface-ui/api/api-detail]’.2020-04-14 13:52:59.716 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[c6eb9f3b3d4c4c8d8a4f807435538172] -&gt; bindType ‘class net.hasor.dataway.web.ApiHistoryListController’ mappingTo: ‘[/interface-ui/api/api-history]’.2020-04-14 13:52:59.717 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[eb841dc72ad54023957233ef602c4327] -&gt; bindType ‘class net.hasor.dataway.web.ApiInfoController’ mappingTo: ‘[/interface-ui/api/api-info]’.2020-04-14 13:52:59.717 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[96aebb46265245459ae21d558e530921] -&gt; bindType ‘class net.hasor.dataway.web.ApiListController’ mappingTo: ‘[/interface-ui/api/api-list]’.2020-04-14 13:52:59.718 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[7467c07f160244df8f228321f6262d3d] -&gt; bindType ‘class net.hasor.dataway.web.ApiHistoryGetController’ mappingTo: ‘[/interface-ui/api/get-history]’.2020-04-14 13:52:59.719 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[97d8da5363c741ba99d87c073a344412] -&gt; bindType ‘class net.hasor.dataway.web.DisableController’ mappingTo: ‘[/interface-ui/api/disable]’.2020-04-14 13:52:59.720 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[8ddc3316ef2642dfa4395ca8ac0fff04] -&gt; bindType ‘class net.hasor.dataway.web.SmokeController’ mappingTo: ‘[/interface-ui/api/smoke]’.2020-04-14 13:52:59.720 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[cc06c5fb343b471aacedc58fb2fe7bf8] -&gt; bindType ‘class net.hasor.dataway.web.SaveApiController’ mappingTo: ‘[/interface-ui/api/save-api]’.2020-04-14 13:52:59.720 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[7915b2b1f89a4e73891edab0264c9bd4] -&gt; bindType ‘class net.hasor.dataway.web.PublishController’ mappingTo: ‘[/interface-ui/api/publish]’.2020-04-14 13:52:59.721 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[0cfa34586455414591bdc389bff23ccb] -&gt; bindType ‘class net.hasor.dataway.web.PerformController’ mappingTo: ‘[/interface-ui/api/perform]’.2020-04-14 13:52:59.721 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[37fe4af3e2994acb8deb72d21f02217c] -&gt; bindType ‘class net.hasor.dataway.web.DeleteController’ mappingTo: ‘[/interface-ui/api/delete]’.当看到 “dataway api workAt /api/” 、 dataway admin workAt /interface-ui/ 信息时,就可以确定 Dataway 的配置已经生效了。 第七步:访问接口管理页面进行接口配置在浏览器中输入 “http://127.0.0.1:8080/interface-ui/” 就可以看到期待已久的界面了。 第八步:新建一个接口Dataway 提供了2中语言模式,我们可以使用强大的 DataQL 查询语言,也可以直接使用 SQL 语言(在 Dataway 内部 SQL 语言也会被转换为 DataQL 的形式执行。) 首先我们在 SQL 模式下尝试执行一条 select 查询,立刻就可以看到这条 SQL 的查询结果。 同样的方式我们使用 DataQL 的方式需要这样写: var query = @@sql()&lt;% select * from interface_info %&gt;return query()其中 var query = @@sql()&lt;% ... %&gt; 是用来定义SQL外部代码块,并将这个定义存入 query 变量名中。 &lt;% %&gt; 中间的就是 SQL 语句。 最后在 DataQL 中调用这个代码块,并返回查询结果。 当接口写好之后就可以保存发布了,为了测试方便,我选用 GET 方式。 接口发布之后我们直接请求:http://127.0.0.1:8080/api/demos,就看到期待已久的接口返回值了。 最后总结经过上面的几个步骤我们介绍了如何基于 Spring Boot 项目使用 Dataway 来简单的配置接口。Dataway 的方式确实给人耳目一新,一个接口竟然可以如此简单的配置出来无需开发任何一行代码,也不需要做任何 Mapping 实体映射绑定。 后面会有更多 Dataway 的文章推出也欢迎大家继续关注,大家在使用过程中遇到什么问题可以在评论区留言,或者加入 Hasor 线下交流群一起讨论(群号请到 Hasor 官网上查阅,这里就卖个关子) 最后放几个有用的连接: Dataway 官方手册:https://www.hasor.net/web/dataway/about.html Dataway 在 OSC 上的项目地址,欢迎收藏:https://www.oschina.net/p/dataway DataQL 手册地址:https://www.hasor.net/web/dataql/what_is_dataql.html Hasor 项目的首页:https://www.hasor.net/web/index.html 原文地址https://my.oschina.net/ta8210/blog/3234639

面试官: 说说你对async的理解

面试官: 说说你对async的理解大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。 分享不易,希望能够得到大家的支持和关注。 TL;DR#async是generator和promise的语法糖,利用迭代器的状态机和promise来进行自更新! 如果懒得往下看,可以看下这个极其简易版本的实现方式: Copy// 复制粘贴即可直接运行function stateMac (arr) { let val; return { next(){ if ((val = arr.shift())) { return { value: val, done: false } else { return { done: true function asyncFn(arr) { const iterator = stateMac(arr); function doSelf () { const cur = iterator.next(); const value = cur.value; if (cur.done) { console.log('done'); return; switch (true) { case value.then &amp;&amp; value.toString() === '[object Promise]': value.then((result) =&gt; { console.log(result); doSelf(); break; case typeof value === 'function': value(); doSelf(); break; default: console.log(value); doSelf(); doSelf(); const mockAsync = [ new Promise((res) =&gt; { setTimeout(function () { res('promise'); }, 3000); function () { console.log('测试'); ];console.log('开始');asyncFn(mockAsync);console.log('结束');前言#async &amp; await 和我们的日常开发紧密相连,但是你真的了解其背后的原理吗? 本文假设你对promise、generator有一定了解。 简述promise#promise就是callback的另一种写法,避免了毁掉地狱,从横向改为纵向,大大提升了可读性和美观。 至于promise的实现,按照promise A+规范一点点写就好了,完成后可以使用工具进行测试,确保你的写的东西是符合规范的。 具体实现原理,市面上有各种各样的写法,我就不多此一举了。 简述generator#generator就不像promise那样,他改变了函数的执行方式。可以理解为协程,就是说多个函数互相配合完成任务。类似于这个东西: Copyfunction generator() { return { _value: [1, 2, 3, 4], next() { return { value: this._value.shift(), done: !this._value.length }const it = generator(); console.log(it.next());console.log(it.next());console.log(it.next());console.log(it.next());这只是一个demo,仅供参考。 具体请参考MDN. async &amp; await#照我的理解,其实就是generator和promise相交的产物,被解析器识别,然后转换成我们熟知的语法。 这次要做的就是去看编译之后的结果是什么样的。 既然如此,我们就带着问题去看,不然看起来也糟心不是~ async包装的函数会返回一个什么样的promise?#Copy// 源代码:async function fn() {} fn();Copy// 编译后变成了一大坨: // generator的polyfillrequire("regenerator-runtime/runtime"); function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); function _asyncToGenerator(fn) { return function() { var self = this, args = arguments; return new Promise(function(resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); _next(undefined); function fn() { return _fn.apply(this, arguments);} function _fn() { _fn = _asyncToGenerator( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() { return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch ((_context.prev = _context.next)) { case 0: case "end": return _context.stop(); }, _callee); ); return _fn.apply(this, arguments);} fn();内容也不是很多,我们一点点来看: generator包装fn内部调用的是_fn,一个私有方法,使用的apply绑定的this,并传入了动态参数。 _fn内调用了_asyncToGenerator方法,由于js调用栈后进先出: 读起来是这样的:fn() =&gt; _asyncToGenerator =&gt; .mark() 执行是反过来的:.mark() =&gt; _asyncToGenerator =&gt; fn() 我们先往里看,映入眼帘的是regeneratorRuntime.mark,该方法是generator的polyfill暴露的方法之一,我们去内部(require('regenerator-runtime/runtime'))简单看下这个mark是用来干什么的。 Copy// 立即执行函数,适配commonjs和浏览器(function (exports) { // 暴露mark方法 exports.mark = function (genFun) { // 兼容判断__proto__,处理老旧环境 if (Object.setPrototypeOf) { Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); } else { genFun.__proto__ = GeneratorFunctionPrototype; // 设置Symbol.toStringTag,适配toString if (!(toStringTagSymbol in genFun)) { genFun[toStringTagSymbol] = 'GeneratorFunction'; // 设置原型 genFun.prototype = Object.create(Gp); return genFun; })(typeof module === 'Object' ? module.exports : {});mark做了两个操作,一个是设置genFun的__proto__,一个是设置prototype,可能有人会好奇: __proto__不是对象上的吗?prototype不是函数上的吗?为啥两个同时应用到一个上面了 这样操作是没问题的,genFun不仅是函数啊,函数还是对象,js中万物皆对象哦。你想想是不是可以通过Function构造函数new出一个函数? 然后开始设置__proto__和prototype,在次之前,我们来简单捋一下原型。 原型下面是个人理解的一个说法,未查阅v8引擎,但是这样是说得通的。如果有问题,欢迎指出,一起沟通,我也会及时修改,以免误导他人!!!。 首先要知道这三个的概念:搞清对象的原型对象(proto)、构造函数的原型(prototype)、构造方法(constructor)。 方便记忆,只需要记住下面几条即可: prototype是构造函数(注意:构造函数也是对象嗷)上特有的属性,代表构造函数的原型。举个例子:有一位小明同学(指代构造函数),他有自己的朋友圈子(指代prototype),通过小明可以找到小红(构造函数.prototype.小红),在通过小红的朋友圈子(prototype)还能找到小蓝,直到有一个人(指代null),孑然一身、无欲无求,莫得朋友。 上面这个关系链就可以理解为原型链。 __proto__是每一个对象上特有的属性,指向当前对象构造函数的prototype。再举个例子:小明家里催的急,不就就生了个大胖小子(通过构造函数{小明}创造出对象{大胖小子}),可以说这个大胖小子一出生就被众星捧月,小明的朋友们纷纷表示,以后孩子有啥事需要帮忙找我就成。这就指代对象上的__proto__,__proto__可以引用构造函数的任何关系。 所以说,代码源于生活~ constructor是啥呢,就是一个prototype上的属性,表示这个朋友圈子是谁的,对于小明来说: 小明.prototype.constructor === 小明。所以,当我们进行继成操作的时候,有必要修正一下constructor,不然朋友圈子就乱了~js中函数和对象有点套娃的意思,万物皆对象,对象又是从构造函数构造而来。对于小明来说,就是我生我生我~~来看两个判断: proto 指向构造当前对象的构造函数的prototype,由于万物皆对象,对象又是通过构造函数构造而来。故Object通过Function构造而来,所以指向了Function.prototype Copyconsole.log(Object.__proto__ === Function.prototype); // =&gt; trueproto 指向构造当前对象的构造函数的prototype,由于万物皆对象,对象又是通过构造函数构造而来。故Function通过Function构造而来,所以指向了Function.prototype Copyconsole.log(Function.__proto__ === Function.prototype); // =&gt; true有兴趣的朋友可以再看看这篇文章 然后,我们再来看看这张图,跟着箭头走一遍,是不是就很清晰了? 继续generator包装mark方法会指定genFun的__proto__和prototype,完完全全替换了genFun的朋友圈以及创造genFun的构造函数的朋友圈,现在genFun就是Generator的克隆品了。 用来设置__proto__ 和 prototype的值,GeneratorFunctionPrototype,GP,我们也简单过一下: // 创建polyfill对象var IteratorPrototype = {};IteratorPrototype[iteratorSymbol] = function () { return this; // 原型相关操作// 获取对象的原型: protovar getProto = Object.getPrototypeOf; // 原生iterator原型var NativeIteratorPrototype = getProto &amp;&amp; getProto(getProto(values([])));// IteratorPrototype设置为原生if ( NativeIteratorPrototype &amp;&amp; NativeIteratorPrototype !== Op &amp;&amp; hasOwn.call(NativeIteratorPrototype, iteratorSymbol) // This environment has a native %IteratorPrototype%; use it instead // of the polyfill. IteratorPrototype = NativeIteratorPrototype; // 创造原型// Gp 为 迭代器原型// IteratorPrototype作为原型对象var Gp = (GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create( IteratorPrototype // 更新构造函数和原型GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;GeneratorFunctionPrototype.constructor = GeneratorFunction; // toString,调用Object.toString.call的时候会返回GeneratorFunctionGeneratorFunctionPrototype[ toStringTagSymbol ] = GeneratorFunction.displayName = 'GeneratorFunction';最后再返回经过处理的genFun,然后再回到mark函数外~ _asyncToGenerator_asyncToGenerator 接收mark处理过的结果: Copy// fn 为 generator 的克隆品function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { // 调用_callee,先看下面,一会在回来哈~ var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep( resolve, reject, _next, _throw, 'next', value function _throw(err) { asyncGeneratorStep( resolve, reject, _next, _throw, 'throw', _next(undefined); }regeneratorRuntime.wrap上面的_asyncToGenerator执行后,会执行mark返回的函数: Copyfunction _callee() { return regeneratorRuntime.wrap(function _callee$( _context // 这里就是动态得了,也就是根据用户写的async函数,转换的记过,由于我们是一个空函数,所以直接stop了 while (1) { switch ((_context.prev = _context.next)) { case 0: case 'end': return _context.stop(); _callee); }_callee会返回wrap处理后的结果,我们继续看: Copy// innerFn是真正执行的函数,outerFn为被mark的函数// self, tryLocsList未传递,为undefinedfunction wrap(innerFn, outerFn, self, tryLocsList) { // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. // outerFn 的原型已经被 mark重新设置,所以会包含generator相关原型 var protoGenerator = outerFn &amp;&amp; outerFn.prototype instanceof Generator ? outerFn : Generator; // 创建自定义原型的对象 var generator = Object.create(protoGenerator.prototype); // context 实例是包含的 this.tryEntries 的 var context = new Context(tryLocsList || []); // The ._invoke method unifies the implementations of the .next, // .throw, and .return methods. generator._invoke = makeInvokeMethod(innerFn, self, context); return generator; }其中有个new Context()的操作,用来重置并记录迭代器的状态,后面会用到。之后给返回generator挂载一个_invoke方法,调用makeInvokeMethod,并传入self(未传递该参数,为undefined)和context。 Copyfunction makeInvokeMethod(innerFn, self, context) { // state只有在该函数中备操作 var state = GenStateSuspendedStart; // GenStateSuspendedStart: 'suspendedStart' // 作为外面的返回值 return function invoke(method, arg) { // 这里就是generator相关的一些操作了,用到的时候再说 }利用闭包初始化state,并返回一个invoke函数,接受两个参数,方法和值。先看到这,继续往后看。 回到之前的_asyncToGenerator: Copy// 返回带有_invoke属性的generator对象var gen = fn.apply(self, args);之后定义了一个next和throw方法,随后直接调用_next开始执行: Copyfunction _next(value) { asyncGeneratorStep( gen, // 迭代器函数 resolve, // promise的resolve reject, // promise的project _next, // 当前函数 _throw, // 下面的_throw函数 'next', // method名 value // arg 参数值 }function _throw(err) { asyncGeneratorStep( resolve, reject, _next, _throw, 'throw', }_next(undefined);其中都是用的asyncGeneratorStep,并传递了一些参数。 那asyncGeneratorStep又是啥呢: Copyfunction asyncGeneratorStep( resolve, reject, _next, _throw, try { var info = gen[key](arg); var value = info.value; } catch (error) { // 出错 reject(error); return; if (info.done) { // 如果完成,直接resolve resolve(value); } else { // 否则,继续下次next调用,形成递归 Promise.resolve(value).then(_next, _throw); }代码很少,获取即将要调用的方法名(key)并传入参数,所以当前info即是: Copyvar info = gen'next';那next是哪来的那?就是之前mark操作中定义的,如果原生支持,就是用原生的迭代器提供的next,否则使用polyfill中定义的next。 还记得之前的makeInvokeMethod吗? 它其实是用来定义标准化next、throw和return的: Copyfunction defineIteratorMethods(prototype) { ['next', 'throw', 'return'].forEach(function (method) { prototype[method] = function (arg) { return this._invoke(method, arg); }// Gp在之前的原型操作有用到defineIteratorMethods(Gp);然后当我们执行的时候,就会走到_invoke定义的invoke方法中: Copyfunction invoke(method, arg) { // 状态判断,抛错 if (state === GenStateExecuting) { throw new Error('Generator is already running'); // 已完成,返回done状态 if (state === GenStateCompleted) { if (method === 'throw') { throw arg; // Be forgiving, per 25.3.3.3.3 of the spec: // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume return doneResult(); // 这里就是之前定义的Context实例,下面代码没啥了,自己看吧 context.method = method; context.arg = arg; while (true) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; if (context.method === 'next') { // Setting context._sent for legacy support of Babel's // function.sent implementation. context.sent = context._sent = context.arg; } else if (context.method === 'throw') { if (state === GenStateSuspendedStart) { state = GenStateCompleted; throw context.arg; context.dispatchException(context.arg); } else if (context.method === 'return') { context.abrupt('return', context.arg); state = GenStateExecuting; // innerFn就是while个循环了,使我们的代码主体 var record = tryCatch(innerFn, self, context); if (record.type === 'normal') { // If an exception is thrown from innerFn, we leave state === // GenStateExecuting and loop back for another invocation. state = context.done ? GenStateCompleted : GenStateSuspendedYield; if (record.arg === ContinueSentinel) { continue; return { value: record.arg, done: context.done } else if (record.type === 'throw') { state = GenStateCompleted; // Dispatch the exception by looping back around to the // context.dispatchException(context.arg) call above. context.method = 'throw'; context.arg = record.arg; };在之后,就是我们熟悉的promise相关操作了,在判断done是否为true,否则继续执行,将_next和_throw作为resolve和reject传入即可。 小结可以看到,仅仅一个async其实做了不少工作。核心就是两个,产出一个兼容版本的generator和使用promise,回到这节的问题上,答案就是: Copyreturn new Promise(function (resolve, reject) {});没错,就是返回一个Promise,内部会根据状态及决定是否继续执行下一个Promise.resolve().then()。 如果async函数内有很多其他操作的代码,那么while会跟着变化,利用prev和next来管理执行顺序。这里就不具体分析了,自己写个例子就明白了~ 可以通过babel在线转换,给自己一个具象的感知,更利于理解。 为什么下面这种函数外的console不会等待,函数内的会等待?#Copyasync function fn() { await (async () =&gt; { await new Promise((r) =&gt; { setTimeout(function () { }, 2000); })(); console.log('你好'); }fn();console.log(123);因为解析后的console.log(123); 是在整个语法糖之外啊,log 和 fn 是主协程序,fn内是辅协程。不相干的。 总结#有句话怎么说来着,会者不难,难者不会。所以人人都是大牛,只是你还没发力而已,哈哈~ 笔者后来思考觉得这种写法完全就是回调函数的替代品,而且增加了空间,加深了调用堆栈,或许原生的写法才是效率最高的吧。 不过,需要良好的编码规范,算是一种折中的方式了。毕竟用这种方式来写业务事半功倍~ 对于本文观点,完全是个人阅读后的思考,如有错误,欢迎指正,我会及时更新,避免误导他人。 拜了个拜~ 作者: 小雨小雨丶 出处:https://www.cnblogs.com/xiaoyuxy/p/12682360.html

掌握使用gitlab ci构建Android包的正确方式

掌握使用gitlab ci构建Android包的正确方式 最近公司在做移动端的项目,自然而然的需要搭建打包的环境。本来计划用Jenkins的,但是发现在gitlab上创建完项目后,提示去配置pipeline,于是决定用gitlab去尝试下,毕竟我觉得Jenkins的配置过于复杂了。 gitlab-runner在gitlab中,gitlab-runner相当于Jenkins中的slave的概念,所以首先需要给项目配置一个runner。gitlab-runner分为三种:Shared Runner、Group Runner、Specific Runner。其中: Shared Runner相当于全局的runner,所有的项目都可以使用。Group Runner相当于给一个分组设置runner,因此,分组中所有的项目都可以使用。Specific Runner顾名思义,就是项目特有的runner。只有这一个项目才能使用。个人建议使用Group Runner,你可以根据不同种类的项目,创建不同的分组,比如:Android、IOS、服务端等,然后针对每个分组创建对应的Group Runner,这样相互之间不会有影响,同时又不需要为每个工程单独创建runner。 安装gitlab-runner需要在你的打包机器上安装gitlab-runner,在不同的平台上,安装gitlab-runner的方式不一致,可以参考官方文档:https://docs.gitlab.com/runner/install/。这里以mac为例: $ sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64 sudo chmod +x /usr/local/bin/gitlab-runner 启动gitlab runner gitlab-runner installgitlab-runner start注册gitlab-runner[root@app3 ~]# sudo gitlab-runner registerRuntime platform arch=amd64 os=linux pid=6324 revision=1b659122 version=12.8.0Running in system-mode.Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):http://192.168.4.194/Please enter the gitlab-ci token for this runner:rbLy6bwsNbTuzD_6Bma4Please enter the gitlab-ci description for this runner: Please enter the gitlab-ci tags for this runner (comma separated):testandroidRegistering runner... succeeded runner=rbLy6bwsPlease enter the executor: parallels, docker-ssh+machine, custom, docker-ssh, shell, ssh, virtualbox, docker+machine, kubernetes, docker:dockerPlease enter the default Docker image (e.g. ruby:2.6):jangrewe/gitlab-ci-androidRunner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!执行上面的命令,就能将gitlab-runner注册到gitlab中,需要说明下执行命令时输入的几个参数,其中gitlab-ci coordinator URL和gitlab-ci token可以从项目(Group需要master权限)的Settings—&gt;CI/CD—&gt;Runners中查看,如下图所示: gitlab-ci description是对runner的描述,根据情况填写即可。 gitlab-ci tags 是runner的标签,在后面的配置文件中会用到。 enter the executor是配置runner使用的执行器,可以是shell,如果你需要执行shell脚本的话。我们这里使用的是docker,使用了一个包含Android环境的容器:jangrewe/gitlab-ci-android。 另外,需要注意,在gitlab所在服务器的/etc/gitlab-runner/config.toml配置文件中也可以对gitlab-runner的注册信息进行修改。 重启gitlab-runner需要注意的是,往gitlab注册一个新的runner之后,需要将gitlab-runner进行重启,这样,注册的runner才能生效。我在操作的时候,因为没有重启,花了好多时间解决出现的问题。其实只要重启即可解决。 检查gitlab-runner的状态配置好之后,可以在项目的settings—&gt;CI/CD中查看新增的runner是否是running状态,如下图所示: 配置Android工程runner配置好后,接下来的工作就是配置Android项目,只需要在工程目录下创建.gitlab-ci.yml配置文件,然后往里面填充你想要实现的步骤即可,有点类似Jenkins中的pipeline脚本。不过,感觉gitlab ci的配置更加简单一些。我项目中的配置文件如下所示: image: gitlab-ci-android:V2 # 用来编译 android 项目的镜像 variables: GRADLE_OPTS: "-Dorg.gradle.daemon=false" # 禁用 gradle 守护进程 before_script: # 配置 gradle 的缓存目录 export GRADLE_USER_HOME=/cache/.gradle chmod +x ./gradlew chmod +x ./update-version-code.sh stages: build 提交代码自动编译 build: stage: build only: - master script: - ./gradlew assembleDebug tags: - android 构建测试包 qa: stage: build only: script: - ./gradlew assembleDebug - sh -x /cache/deploy-android.sh artifacts: paths: - app/build/outputs/apk/debug/ tags: - android 上面配置的大致意思是:当开发往qa分支提交代码时,会触发构建测试包,打包完成后,会将包上传到一个文件服务器上,方便下载安装。 最后上一个gitlab的效果图: 总结整体来看,是不是挺简单的,大家可以直接在gitlab上玩ci/cd,大大降低了成本。所以如果大家现在还没有将CI/CD做起来的,大家可以考虑直接用gitlab来玩。 原文地址https://www.cnblogs.com/zhouliweiblog/p/12677635.html

一文教会你如何在 Spring 中进行集成测试,太赞了

一文教会你如何在 Spring 中进行集成测试,太赞了 不得不说,测试真的是太重要了!但并不是所有的开发者都这样认为,这种感觉在我回到洛阳后尤其强烈。竟然有团队成员不经测试就把代码提交到代码库,并且是会报错的那种,我天呐,遇到这种队友我也是醉了。 我之前是在一家日企工作,他们非常注重测试,占用的时间比代码编写的时间多多了。从单元测试到集成测试,所有的测试结果都要整理成文档保存下来,哪怕你觉得完全没有必要。但正是这种一丝不苟的态度,成就了日企软件高质量的美誉。 单元测试通常比较简单,对运行环境的依赖性不强。但集成测试就完全不同了,需要整个项目是能够跑起来的,比如说需要数据库是连接的,网络是通畅的等等。 集成测试最简单的形式是:把两个已经测试过的单元组合成一个组件,测试它们之间的接口。从这一层意义上讲,组件是指多个单元的集成聚合。在现实方案中,许多单元组合成组件,而这些组件又聚合为程序的更大部分。方法是测试片段的组合,并最终扩展成进程,将模块与其他组的模块一起测试。最后,将构成进程的所有模块一起测试。 Spring 提供了 Spring TestContex Framework 来支持集成测试,它不依赖于特定的测试框架,因此你可以选择 Junit,也可以选择 TestNG。本文选择 Junit,因此需要先将 Junit 和 Spring TestContex Framework 的依赖添加到 pom.xml 文件中。     org.springframework    spring-test    5.2.2.RELEASE    test     junit    junit    4.12    test准备工作完成后,来考虑这样一个场景,我们需要同时为开发环境和生产环境维护数据源配置,因此我们先创建一个公共的接口 Datasource,内容如下所示: public interface Datasource {    public void setup();}新建 DevDatasource 类,实现 Datasource 接口,该类用于为开发环境配置数据源,内容如下: @Component@Profile("dev")public class DevDatasource implements Datasource {    @Override    public void setup() {        System.out.println("开发环境");    }}@Component 注解用于为 Spring 容器注入一个通用的 Bean 组件。@Profile 注解用于标识不同环境下要实例化的 Bean,字符串“dev” 表示该组件用于开发环境。 新建 ProdDatasource 类,实现 Datasource 接口,该类用于为正式运行环境配置数据源,内容如下: @Component@Profile("prod")public class ProdDatasource implements Datasource {    @Override    public void setup() {        System.out.println("正式环境");    }}新建配置类 SpringTestConfig,内容如下: @Configuration@ComponentScan("test")public class SpringTestConfig {}@Configuration 注解表示当前类为一个配置类,@ComponentScan 注解用于指定扫描路径,该路径下的 Bean 将会自动装配到 Spring 容器中。 基于 Maven 构建的项目默认有两个测试目录,src/test/java 和 src/test/resources,分别对应于 src/main/java 和 src/main/resources。前者用于测试项目源码,后者用于测试项目资源。 我们在 src/test/java 目录下新建测试类 DevTest,内容如下: @RunWith(SpringJUnit4ClassRunner.class)@ActiveProfiles("dev")@ContextConfiguration(classes = { SpringTestConfig.class })public class DevTest {    @Autowired    Datasource datasource;     @Test    public void testSpringProfiles() {        Assert.assertTrue(datasource instanceof DevDatasource);    }}1)@RunWith 注解用于指定 Junit 运行环境,是 Junit 提供给其他框架测试环境的接口扩展,Spring 提供了 org.springframework.test.context.junit4.SpringJUnit4ClassRunner 作为 Junit 测试环境。 2)@ActiveProfile 注解用于指定哪个配置文件处于活动状态,本例为开发环境“dev”。 3)@ContextConfiguration 注解用于指定配置类,本例为 SpringTestConfig 类。 4)@Autowired 注解用于指定 Spring 要自动装配的 Bean。 5)@Test 注解用于表示当前方法为 Junit 测试方法。 程序运行的结果如下图所示: 再新建一个测试类 ProdTest,内容如下: @RunWith(SpringJUnit4ClassRunner.class)@ActiveProfiles("prod")@ContextConfiguration(classes = { SpringTestConfig.class })public class ProdTest {     @Autowired    Datasource datasource;     @Autowired    Environment environment;     @Test    public void testSpringProfiles() {        for (String profileName : environment.getActiveProfiles()) {            System.out.println("当前激活的 profile - " + profileName);        }        Assert.assertTrue(datasource instanceof ProdDatasource);    }}这次使用 @ActiveProfiles("prod") 注解指定当前环境为正式运行下的环境。本例中我们装配了一个新的 Bean environment,使用它的 getActiveProfiles() 方法可以获取当前激活的 Profile。程序运行的结果如下图所示: 好了,我亲爱的读者朋友,以上就是本文的全部内容了,是不是感觉在 Spring 中进行集成测试还是挺简单的?如果觉得文章对你有点帮助,请微信搜索「 沉默王二 」第一时间阅读。示例代码已经上传到 GitHub,传送门~ 原文地址https://www.cnblogs.com/qing-gee/p/12671272.html

[ASP.NET Core MVC] 如何实现运行时动态定义Controller类型?

&lt;form method="post"&gt; &lt;textarea name="source" cols="50" rows="10"&gt;Define your controller here...&lt;/textarea&gt; &lt;br/&gt; &lt;button type="submit"&gt;Register&lt;/button&gt; &lt;/form&gt;

[译]Spring中的设计模式

[译]Spring中的设计模式 1.介绍#设计模式是软件开发的重要组成部分。这些解决方案不仅解决了反复出现的问题,而且还通过识别通用模式来帮助开发人员了解框架的设计。 在本教程中,我们将研究Spring框架中使用的四种最常见的设计模式: 单例模式工厂方法模式代理模式模板模式我们还将研究Spring如何使用这些模式来减轻开发人员的负担并帮助用户快速执行繁琐的任务。 2.单例模式#单例模式是一种确保每个应用程序仅存在一个对象实例的机制。在管理共享资源或提供跨领域服务(例如日志记录)时,此模式很有用。 2.1 单例beans#通常,单例对于应用程序是全局唯一的,但是在Spring中,此约束更宽泛。Spring定义的单例是在spring IOC容器中唯一。实际上,这意味着Spring只会为每个应用程序上下文的每种类型创建一个bean。 Spring的方法不同于严格的单例定义,因为一个应用程序可以具有多个Spring容器。因此,如果我们有多个容器,则同一类的多个对象可以在单个应用程序中存在。 默认情况下,Spring将所有bean创建为单例。 2.2 自动装配单例对象#例如,我们可以在一个应用程序上下文中创建两个控制器,并将相同类型的bean注入每个控制器中。 首先,我们创建一个BookRepository管理我们的Book域对象。 接下来,我们创建LibraryController,它使用BookRepository返回库中的书数: Copy@RestControllerpublic class LibraryController { @Autowired private BookRepository repository; @GetMapping("/count") public Long findCount() { System.out.println(repository); return repository.count(); }最后,我们创建一个BookController,专注于特定于图书的操作,例如通过其ID查找一本书: Copy@RestControllerpublic class BookController { @Autowired private BookRepository repository; @GetMapping("/book/{id}") public Book findById(@PathVariable long id) { System.out.println(repository); return repository.findById(id).get(); }然后,我们启动此应用程序并在/ count和/ book / 1上执行GET: Copycurl -X GET http://localhost:8080/countcurl -X GET http://localhost:8080/book/1在应用程序输出中,我们看到两个BookRepository对象具有相同的对象ID: Copycom.baeldung.spring.patterns.singleton.BookRepository@3ea9524fcom.baeldung.spring.patterns.singleton.BookRepository@3ea9524fLibraryController和BookController中的BookRepository对象ID相同,这证明Spring将相同的bean注入到两个控制器中。 我们可以使用@scope(ConfigurableBeanFactory.scope_prototype)注释将bean范围从singleton更改为prototype,从而创建BookRepository bean的单独实例。 这样做指示Spring为它创建的每个BookRepository Bean创建单独的对象。因此,如果我们再次检查每个控制器中BookRepository的对象ID,我们将发现它们不再相同。 3.工厂方法模式#工厂方法模式要求工厂类具有用于创建所需对象的抽象方法。 通常,我们想基于特定的上下文创建不同的对象。 例如,我们的应用程序可能需要车辆对象。在航海环境中,我们想要制造船只,但是在航空航天环境中,我们想要制造飞机: 为此,我们可以为每个所需的对象创建一个工厂实现,并从具体的工厂方法中返回所需的对象。 3.1 Application Context#Spring在其依赖注入(DI)框架的基础上使用了此技术。 从根本上讲,Spring将一个bean容器视为生成bean的工厂。 因此,Spring将BeanFactory接口定义为Bean容器的抽象: Copypublic interface BeanFactory { getBean(Class&lt;T&gt; requiredType); getBean(Class&lt;T&gt; requiredType, Object... args); getBean(String name); // ... }每个getBean方法均被视为工厂方法,该方法将返回一个与提供给该方法的条件相匹配的bean,例如bean的类型和名称。 然后,Spring使用ApplicationContext接口扩展BeanFactory,该接口引入了其他应用程序配置。 Spring使用此配置基于一些外部配置(例如XML文件或Java批注)来启动Bean容器。 然后使用诸如AnnotationConfigApplicationContext之类的ApplicationContext类实现,我们可以通过从BeanFactory接口继承的各种工厂方法来创建bean。 首先,我们创建一个简单的应用程序配置: Copy@Configuration@ComponentScan(basePackageClasses = ApplicationConfig.class)public class ApplicationConfig {}接下来,我们创建一个简单的类Foo,它不接受构造函数参数: Copy@Componentpublic class Foo {}然后创建另一个接受单个构造函数参数的类Bar: Copy@Component@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)public class Bar { private String name; public Bar(String name) { this.name = name; // Getter ... }最后,我们通过ApplicationContext的AnnotationConfigApplicationContext实现创建我们的bean: Copy@Testpublic void whenGetSimpleBean_thenReturnConstructedBean() { ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class); Foo foo = context.getBean(Foo.class); assertNotNull(foo); @Testpublic void whenGetPrototypeBean_thenReturnConstructedBean() { String expectedName = "Some name"; ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class); Bar bar = context.getBean(Bar.class, expectedName); assertNotNull(bar); assertThat(bar.getName(), is(expectedName)); }使用getBean工厂方法,我们可以仅使用类类型和构造函数参数(对于Bar而言)来创建已配置的bean。 3.2外部配置#这种模式是通用的,因为我们可以根据外部配置完全更改应用程序的行为。 如果我们希望更改应用程序中自动装配对象的实现,则可以调整我们使用的ApplicationContext实现。 例如,我们可以将AnnotationConfigApplicationContext更改为基于XML的配置类,例如ClassPathXmlApplicationContext: Copy@Testpublic void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() { String expectedName = "Some name"; ApplicationContext context = new ClassPathXmlApplicationContext("context.xml"); // Same test as before ... }4.代理模式#代理在我们的数字世界中是一种方便的工具,我们经常在软件(例如网络代理)之外使用它们。在代码中,代理模式是一种技术,它允许一个对象(代理)控制对另一对象(主题或服务)的访问。 4.1 事务#要创建代理,我们创建一个对象,该对象实现与主体相同的接口,并包含对该主体的引用。 然后,我们可以使用代理代替主体。 在Spring中,代理Bean以控制对基础Bean的访问。我们在使用事务时会看到这种方法: Copy@Servicepublic class BookManager { @Autowired private BookRepository repository; @Transactional public Book create(String author) { System.out.println(repository.getClass().getName()); return repository.create(author); }在我们的BookManager类中,我们使用@Transactional注释对create方法进行注释。该注释指示Spring自动执行我们的create方法。没有代理,Spring将无法控制对我们的BookRepository bean的访问并确保其事务一致性。 4.2 CGLib代理#相反,Spring创建了一个代理,该代理包装了我们的BookRepository bean,并检测了我们的bean以自动执行我们的create方法。 当我们调用BookManager#create方法时,我们可以看到输出: Copycom.baeldung.patterns.proxy.BookRepository$$EnhancerBySpringCGLIB$$3dc2b55c通常,我们希望看到一个标准的BookRepository对象ID。相反,我们看到了EnhancerBySpringCGLIB对象ID。 在后台,Spring将我们的BookRepository对象包装为EnhancerBySpringCGLIB对象。 Spring因此控制对BookRepository对象的访问(确保事务一致性)。 通常,Spring使用两种类型的代理: CopyCGLib Proxies – Used when proxying classesJDK Dynamic Proxies – Used when proxying interfaces当我们使用事务公开底层代理时,Spring将在必须控制对bean的访问的任何情况下使用代理。 5.模板模式#在许多框架中,大部分代码是样板代码。 例如,在数据库上执行查询时,必须完成相同的一系列步骤: 建立连接执行查询执行清理关闭连接这些步骤是模板方法模式的理想场景。 5.1 模板和回调#模板方法模式是一种定义某些操作所需的步骤,实现样板步骤并将可自定义步骤保留为抽象的技术。然后,子类可以实现此抽象类,并为缺少的步骤提供具体的实现。 对于数据库查询,我们可以创建一个模板: Copypublic abstract DatabaseQuery { public void execute() { Connection connection = createConnection(); executeQuery(connection); closeConnection(connection); protected Connection createConnection() { // Connect to database... protected void closeConnection(Connection connection) { // Close connection... protected abstract void executeQuery(Connection connection); }另外,我们可以通过提供回调方法来提供缺少的步骤。 回调方法是一种允许主体向客户端发信号通知某些所需操作已完成的方法。 在某些情况下,主体可以使用此回调执行操作-例如映射结果。 例如,代替使用executeQuery方法,我们可以为execute方法提供查询字符串和回调方法来处理结果。 首先,我们创建一个回调方法,该方法接受一个Results对象并将其映射到T类型的对象: Copypublic interface ResultsMapper { public T map(Results results); }然后我们更改我们的DatabaseQuery类以利用此回调: Copypublic abstract DatabaseQuery { public &lt;T&gt; T execute(String query, ResultsMapper&lt;T&gt; mapper) { Connection connection = createConnection(); Results results = executeQuery(connection, query); closeConnection(connection); return mapper.map(results); protected Results executeQuery(Connection connection, String query) { // Perform query... }这种回调机制正是Spring与JdbcTemplate类一起使用的方法。 5.2 JdbcTemplate#JdbcTemplate类提供了查询方法,该方法接受查询String和ResultSetExtractor对象: Copypublic class JdbcTemplate { public &lt;T&gt; T query(final String sql, final ResultSetExtractor&lt;T&gt; rse) throws DataAccessException { // Execute query... // Other methods... }ResultSetExtractor将代表查询结果的ResultSet对象转换为T类型的域对象: Copy@FunctionalInterfacepublic interface ResultSetExtractor { T extractData(ResultSet rs) throws SQLException, DataAccessException; }通过创建更多特定的回调接口,Spring进一步减少了样板代码。 例如,RowMapper接口用于将单行SQL数据转换为类型T的域对象。 Copypublic class JdbcTemplate { public &lt;T&gt; List&lt;T&gt; query(String sql, RowMapper&lt;T&gt; rowMapper) throws DataAccessException { return result(query(sql, new RowMapperResultSetExtractor&lt;&gt;(rowMapper))); // Other methods... }除了提供用于转换整个ResultSet对象(包括在行上进行迭代)的逻辑之外,我们可以提供用于如何转换单行的逻辑: Copypublic class BookRowMapper implements RowMapper { @Override public Book mapRow(ResultSet rs, int rowNum) throws SQLException { Book book = new Book(); book.setId(rs.getLong("id")); book.setTitle(rs.getString("title")); book.setAuthor(rs.getString("author")); return book; }使用此转换器,我们可以使用JdbcTemplate查询数据库并映射每个结果行: CopyJdbcTemplate template = // create template...template.query("SELECT * FROM books", new BookRowMapper());除了JDBC数据库管理,Spring还使用以下模板: Java Message Service (JMS)Java Persistence API (JPA)Hibernate (now deprecated)Transactions6.总结#在本教程中,我们研究了Spring框架中应用的四种最常见的设计模式。 我们还探讨了Spring如何利用这些模式来提供丰富的功能,同时减轻开发人员的负担。 作者: 东溪陈姓少年 出处:https://www.cnblogs.com/dongxishaonian/p/12641064.html

var oBtn = document.getElementsByTagName('button')[0], oList = document.getElementsByTagName('ul')[0], oLi = oList.getElementsByTagName('li'); oBtn.onclick = function () { var li = document.createElement('li'); li.innerText = oLi.length + 1; oList.appendChild(li); oList.onclick = function (ev) { var ev = ev || window.event, tar = ev.target || ev.srcElement; // tar 即为被点击的 li 元素 console.log(tar.innerHTML); // 返回在所有兄弟元素中的索引,借用数组 indexOf 方法 console.log(Array.prototype.indexOf.call(oLi, tar));

MySQL的死锁系列- 锁的类型以及加锁原理

MySQL的死锁系列- 锁的类型以及加锁原理疫情期间在家工作时,同事使用了 insert into on duplicate key update 语句进行插入去重,但是在测试过程中发现了死锁现象: ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction由于开发任务紧急,只是暂时规避了一下,但是对触发死锁的原因和相关原理不甚了解,于是这几天一直在查阅相关资料,总结出一个系列文章供大家参考。本篇是上篇,主要介绍 MySQL 加锁原理和锁的不同模式或类型的基本知识。后续会讲解常见语句的加锁情况和通过 MySQL 死锁日志分析死锁原因。 由于本篇文章涉及很多 MySQL 的基础知识,大家可以自行阅读我之前的 MySQL系列文章 《MySQL探秘》中的对应章节。 表锁和行锁我们首先来了解一下表锁和行锁:表锁是指对一整张表加锁,一般是 DDL 处理时使用;而行锁则是锁定某一行或者某几行,或者行与行之间的间隙。 表锁由 MySQL Server 实现,行锁则是存储引擎实现,不同的引擎实现的不同。在 MySQL 的常用引擎中 InnoDB 支持行锁,而 MyISAM 则只能使用 MySQL Server 提供的表锁。 表锁表锁由 MySQL Server 实现,一般在执行 DDL 语句时会对整个表进行加锁,比如说 ALTER TABLE 等操作。在执行 SQL 语句时,也可以明确指定对某个表进行加锁。 mysql&gt; lock table user read(write); # 分为读锁和写锁Query OK, 0 rows affected (0.00 sec) mysql&gt; select * from user where id = 100; # 成功mysql&gt; select * from role where id = 100; # 失败,未提前获取该 role的读表锁mysql&gt; update user set name = 'Tom' where id = 100; # 失败,未提前获得user的写表锁 mysql&gt; unlock tables; # 显示释放表锁Query OK, 0 rows affected (0.00 sec)表锁使用的是一次性锁技术,也就是说,在会话开始的地方使用 lock 命令将后续需要用到的表都加上锁,在表释放前,只能访问这些加锁的表,不能访问其他表,直到最后通过 unlock tables 释放所有表锁。 除了使用 unlock tables 显示释放锁之外,会话持有其他表锁时执行lock table 语句会释放会话之前持有的锁;会话持有其他表锁时执行 start transaction 或者 begin 开启事务时,也会释放之前持有的锁。 行锁不同存储引擎的行锁实现不同,后续没有特别说明,则行锁特指 InnoDB 实现的行锁。 在了解 InnoDB 的加锁原理前,需要对其存储结构有一定的了解。InnoDB 是聚簇索引,也就是 B+树的叶节点既存储了主键索引也存储了数据行。而 InnoDB 的二级索引的叶节点存储的则是主键值,所以通过二级索引查询数据时,还需要拿对应的主键去聚簇索引中再次进行查询。关于 InnoDB 和 MyISAM 的索引的详细知识可以阅读《Mysql探索(一):B+Tree索引》一文。 下面以两条 SQL 的执行为例,讲解一下 InnoDB 对于单行数据的加锁原理。 update user set age = 10 where id = 49;update user set age = 10 where name = 'Tom';第一条 SQL 使用主键索引来查询,则只需要在 id = 49 这个主键索引上加上写锁;第二条 SQL 则使用二级索引来查询,则首先在 name = Tom 这个索引上加写锁,然后由于使用 InnoDB 二级索引还需再次根据主键索引查询,所以还需要在 id = 49 这个主键索引上加写锁,如上图所示。 也就是说使用主键索引需要加一把锁,使用二级索引需要在二级索引和主键索引上各加一把锁。 根据索引对单行数据进行更新的加锁原理了解了,那如果更新操作涉及多个行呢,比如下面 SQL 的执行场景。 update user set age = 10 where id &gt; 49;上述 SQL 的执行过程如下图所示。MySQL Server 会根据 WHERE 条件读取第一条满足条件的记录,然后 InnoDB 引擎会将第一条记录返回并加锁,接着 MySQL Server 发起更新改行记录的 UPDATE 请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有匹配的记录为止。 这种场景下的锁的释放较为复杂,有多种的优化方式,我对这块暂时还没有了解,还请知道的小伙伴在下方留言解释。 下面主要依次介绍 InnoDB 中锁的模式和类型,锁的类型是指锁的粒度或者锁具体加在什么地方;而锁模式描述的是锁的兼容性,也就是加的是什么锁,比如写锁或者读锁。 内容基本来自于 MySQL 的技术文档 innodb-lock 一章,感兴趣的同学可以直接去阅读原文,原文地址为见文章末尾。 行锁的模式锁的模式有:读意向锁,写意向锁,读锁,写锁和自增锁(auto_inc),下面我们依次来看。 读写锁读锁,又称共享锁(Share locks,简称 S 锁),加了读锁的记录,所有的事务都可以读取,但是不能修改,并且可同时有多个事务对记录加读锁。 写锁,又称排他锁(Exclusive locks,简称 X 锁),或独占锁,对记录加了排他锁之后,只有拥有该锁的事务可以读取和修改,其他事务都不可以读取和修改,并且同一时间只能有一个事务加写锁。 读写意向锁由于表锁和行锁虽然锁定范围不同,但是会相互冲突。所以当你要加表锁时,势必要先遍历该表的所有记录,判断是否加有排他锁。这种遍历检查的方式显然是一种低效的方式,MySQL 引入了意向锁,来检测表锁和行锁的冲突。 意向锁也是表级锁,也可分为读意向锁(IS 锁)和写意向锁(IX 锁)。当事务要在记录上加上读锁或写锁时,要首先在表上加上意向锁。这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就行了。 意向锁之间是不会产生冲突的,也不和 AUTO_INC 表锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁也不会和行锁冲突,行锁只会和行锁冲突。 自增锁AUTO_INC 锁又叫自增锁(一般简写成 AI 锁),是一种表锁,当表中有自增列(AUTO_INCREMENT)时出现。当插入表中有自增列时,数据库需要自动生成自增值,它会先为该表加 AUTO_INC 表锁,阻塞其他事务的插入操作,这样保证生成的自增值肯定是唯一的。AUTO_INC 锁具有如下特点: AUTO_INC 锁互不兼容,也就是说同一张表同时只允许有一个自增锁;自增值一旦分配了就会 +1,如果事务回滚,自增值也不会减回去,所以自增值可能会出现中断的情况。显然,AUTO_INC 表锁会导致并发插入的效率降低,为了提高插入的并发性,MySQL 从 5.1.22 版本开始,引入了一种可选的轻量级锁(mutex)机制来代替 AUTO_INC 锁,可以通过参数 innodb_autoinc_lock_mode 来灵活控制分配自增值时的并发策略。具体可以参考 MySQL 的 AUTO_INCREMENT Handling in InnoDB 一文,链接在文末。 不同模式锁的兼容矩阵下面是各个表锁之间的兼容矩阵。 总结起来有下面几点: 意向锁之间互不冲突;S 锁只和 S/IS 锁兼容,和其他锁都冲突;X 锁和其他所有锁都冲突;AI 锁只和意向锁兼容;行锁的类型根据锁的粒度可以把锁细分为表锁和行锁,行锁根据场景的不同又可以进一步细分,依次为 Next-Key Lock,Gap Lock 间隙锁,Record Lock 记录锁和插入意向 GAP 锁。 不同的锁锁定的位置是不同的,比如说记录锁只锁住对应的记录,而间隙锁锁住记录和记录之间的间隔,Next-Key Lock 则所属记录和记录之前的间隙。不同类型锁的锁定范围大致如下图所示。 下面我们来依次了解一下不同的类型的锁。 记录锁记录锁是最简单的行锁,并没有什么好说的。上边描述 InnoDB 加锁原理中的锁就是记录锁,只锁住 id = 49 或者 name = 'Tom' 这一条记录。 当 SQL 语句无法使用索引时,会进行全表扫描,这个时候 MySQL 会给整张表的所有数据行加记录锁,再由 MySQL Server 层进行过滤。但是,在 MySQL Server 层进行过滤的时候,如果发现不满足 WHERE 条件,会释放对应记录的锁。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。 所以更新操作必须要根据索引进行操作,没有索引时,不仅会消耗大量的锁资源,增加数据库的开销,还会极大的降低了数据库的并发性能。 间隙锁还是最开始更新用户年龄的例子,如果 id = 49 这条记录不存在,这个 SQL 语句还会加锁吗?答案是可能有,这取决于数据库的隔离级别。这种情况下,在 RC 隔离级别不会加任何锁,在 RR 隔离级别会在 id = 49 前后两个索引之间加上间隙锁。 间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。这个间隙可以跨一个索引记录,多个索引记录,甚至是空的。使用间隙锁可以防止其他事务在这个范围内插入或修改记录,保证两次读取这个范围内的记录不会变,从而不会出现幻读现象。 值得注意的是,间隙锁和间隙锁之间是互不冲突的,间隙锁唯一的作用就是为了防止其他事务的插入,所以加间隙 S 锁和加间隙 X 锁没有任何区别。 Next-Key 锁Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。假设一个索引包含15、18、20 ,30,49,50 这几个值,可能的 Next-key 锁如下: (-∞, 15],(15, 18],(18, 20],(20, 30],(30, 49],(49, 50],(50, +∞)通常我们都用这种左开右闭区间来表示 Next-key 锁,其中,圆括号表示不包含该记录,方括号表示包含该记录。前面四个都是 Next-key 锁,最后一个为间隙锁。和间隙锁一样,在 RC 隔离级别下没有 Next-key 锁,只有 RR 隔离级别才有。还是之前的例子,如果 id 不是主键,而是二级索引,且不是唯一索引,那么这个 SQL 在 RR 隔离级别下就会加如下的 Next-key 锁 (30, 49](49, 50) 此时如果插入一条 id = 31 的记录将会阻塞住。之所以要把 id = 49 前后的间隙都锁住,仍然是为了解决幻读问题,因为 id 是非唯一索引,所以 id = 49 可能会有多条记录,为了防止再插入一条 id = 49 的记录。 插入意向锁插入意向锁是一种特殊的间隙锁(简写成 II GAP)表示插入的意向,只有在 INSERT 的时候才会有这个锁。注意,这个锁虽然也叫意向锁,但是和上面介绍的表级意向锁是两个完全不同的概念,不要搞混了。 插入意向锁和插入意向锁之间互不冲突,所以可以在同一个间隙中有多个事务同时插入不同索引的记录。譬如在上面的例子中,id = 30 和 id = 49 之间如果有两个事务要同时分别插入 id = 32 和 id = 33 是没问题的,虽然两个事务都会在 id = 30 和 id = 50 之间加上插入意向锁,但是不会冲突。 插入意向锁只会和间隙锁或 Next-key 锁冲突,正如上面所说,间隙锁唯一的作用就是防止其他事务插入记录造成幻读,正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。 不同类型锁的兼容矩阵不同类型锁的兼容下如下图所示。 其中,第一行表示已有的锁,第一列表示要加的锁。插入意向锁较为特殊,所以我们先对插入意向锁做个总结,如下: 插入意向锁不影响其他事务加其他任何锁。也就是说,一个事务已经获取了插入意向锁,对其他事务是没有任何影响的;插入意向锁与间隙锁和 Next-key 锁冲突。也就是说,一个事务想要获取插入意向锁,如果有其他事务已经加了间隙锁或 Next-key 锁,则会阻塞。其他类型的锁的规则较为简单: 间隙锁不和其他锁(不包括插入意向锁)冲突;记录锁和记录锁冲突,Next-key 锁和 Next-key 锁冲突,记录锁和 Next-key 锁冲突;原文地址https://www.cnblogs.com/remcarpediem/p/12616503.html

走近源码:Redis如何清除过期key

走近源码:Redis如何清除过期key“叮……”,美好的周六就这么被一阵钉钉消息吵醒了。 业务组的同学告诉我说很多用户的帐号今天被强制下线。我们的帐号系统正常的逻辑是用户登录一次后,token的有效期可以维持一天的时间。现在的问题是用户大概每10分钟左右就需要重新登录一次。这种情况一般有两种原因:1、token生成时出问题。2、验证token时出现问题。 通过检查日志,我发现是验证token时,Redis中已经没有对应的token了。并且确定了生成新的token时,set到Redis中的有效期是正确的,那么就基本可以确定是Redis的问题了。 于是又去检查了Redis的监控,发现在那段时间Redis由于内存占用过高强制清理了几次key。但从日志上来看,这段时间并没有出现流量暴涨的情况,而且Redis中key的数量也没有显著增加。那是什么原因导致Redis内存占用过高呢?确定了Redis内存升高不是我们造成的之后,我们又联系了业务组的同学协助他们,他们表示最近确实有上线,并且新上线的功能有使用到Redis。但我仍然感觉很奇怪,为什么Redis中的key没有增多,并且没看到有其他业务的key。经过一番询问,才了解到,业务组同学使用的是这个Redis的db1,而我用的(和刚查的)是db0。这里确实是我在排查问题时出现了疏忽。 那么Redis的不同db之间会互相影响吗?通常情况下,我们使用不同的db进行数据隔离,这没问题。但Redis进行清理时,并不是只清理数据量占用最大的那个db,而是会对所有的db进行清理。在这之前我并不是很了解这方面知识,这里也只是根据现象进行的猜测。 好奇心驱使我来验证一下这个想法。于是我决定直接来看Redis的源码。清理key相关的代码在evict.c文件中。 Redis中会保存一个“过期key池”,这个池子中存放了一些可能会被清理的key。其中保存的数据结构如下: struct evictionPoolEntry { unsigned long long idle; /* Object idle time (inverse frequency for LFU) */ sds key; /* Key name. */ sds cached; /* Cached SDS object for key name. */ int dbid; /* Key DB number. */ };其中idle是对象空闲时间,在Reids中,key的过期算法有两种:一种是近似LRU,一种是LFU。默认使用的是近似LRU。 近似LRU在解释近似LRU之前,先来简单了解一下LRU。当Redis的内存占用超过我们设置的maxmemory时,会把长时间没有使用的key清理掉。按照LRU算法,我们需要对所有key(也可以设置成只淘汰有过期时间的key)按照空闲时间进行排序,然后淘汰掉空闲时间最大的那部分数据,使得Redis的内存占用降到一个合理的值。 LRU算法的缺点是,我们需要维护一个全部(或只有过期时间)key的列表,还要按照最近使用时间排序。这会消耗大量内存,并且每次使用key时更新排序也会占用额外的CPU资源。对于Redis这样对性能要求很高的系统来说是不被允许的。 因此,Redis采用了一种近似LRU的算法。当Redis接收到新的写入命令,而内存又不够时,就会触发近似LRU算法来强制清理一些key。具体清理的步骤是,Redis会对key进行采样,通常是取5个,然后会把过期的key放到我们上面说的“过期池”中,过期池中的key是按照空闲时间来排序的,Redis会优先清理掉空闲时间最长的key,直到内存小于maxmemory。 近似LRU算法的清理效果图如图(图片来自Redis官方文档) 这么说可能不够清楚,我们直接上代码。 上图展示了代码中近似LRU算法的主要逻辑调用路径。 其中主要逻辑是在freeMemoryIfNeeded函数中 首先调用getMaxmemoryState函数判断当前内存的状态 int getMaxmemoryState(size_t total, size_t logical, size_t tofree, float level) { size_t mem_reported, mem_used, mem_tofree; mem_reported = zmalloc_used_memory(); if (total) *total = mem_reported; int return_ok_asap = !server.maxmemory || mem_reported &lt;= server.maxmemory; if (return_ok_asap &amp;&amp; !level) return C_OK; mem_used = mem_reported; size_t overhead = freeMemoryGetNotCountedMemory(); mem_used = (mem_used &gt; overhead) ? mem_used-overhead : 0; if (level) { if (!server.maxmemory) { *level = 0; } else { *level = (float)mem_used / (float)server.maxmemory; if (return_ok_asap) return C_OK; if (mem_used &lt;= server.maxmemory) return C_OK; mem_tofree = mem_used - server.maxmemory; if (logical) *logical = mem_used; if (tofree) *tofree = mem_tofree; return C_ERR; }如果使用内存低于maxmemory的话,就返回C_OK,否则返回C_ERR。另外,这个函数还通过传递指针型的参数来返回一些额外的信息。 total:已使用的字节总数,无论是C_OK还是C_ERR都有效。logical:已使用的内存减去slave或AOF缓冲区后的大小,只有返回C_ERR时有效。tofree:需要释放的内存大小,只有返回C_ERR时有效。level:已使用内存的比例,通常是0到1之间,当超出内存限制时,就大于1。无论是C_OK还是C_ERR都有效。判断完内存状态以后,如果内存没有超过使用限制就会直接返回,否则就继续向下执行。此时我们已经知道需要释放多少内存空间了,下面就开始进行释放内存的操作了。每次释放内存都会记录释放内存的大小,直到释放的内存不小于tofree。 首先根据maxmemory_policy进行判断,对于不同的清除策略有不同的实现方法,我们来看LRU的具体实现。 for (i = 0; i &lt; server.dbnum; i++) { db = server.db+i; dict = (server.maxmemory_policy &amp; MAXMEMORY_FLAG_ALLKEYS) ? db-&gt;dict : db-&gt;expires; if ((keys = dictSize(dict)) != 0) { evictionPoolPopulate(i, dict, db-&gt;dict, pool); total_keys += keys; }}首先是填充“过期池”,这里遍历了每一个db(验证了我最开始的想法),调用evictionPoolPopulate函数进行填充。 void evictionPoolPopulate(int dbid, dict sampledict, dict keydict, struct evictionPoolEntry *pool) { int j, k, count; dictEntry *samples[server.maxmemory_samples]; count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples); for (j = 0; j &lt; count; j++) { unsigned long long idle; sds key; robj *o; dictEntry *de; de = samples[j]; key = dictGetKey(de); /* some code */ if (server.maxmemory_policy &amp; MAXMEMORY_FLAG_LRU) { idle = estimateObjectIdleTime(o); /* some code */ k = 0; while (k &lt; EVPOOL_SIZE &amp;&amp; pool[k].key &amp;&amp; pool[k].idle &lt; idle) k++; if (k == 0 &amp;&amp; pool[EVPOOL_SIZE-1].key != NULL) { continue; } else if (k &lt; EVPOOL_SIZE &amp;&amp; pool[k].key == NULL) { } else { if (pool[EVPOOL_SIZE-1].key == NULL) { sds cached = pool[EVPOOL_SIZE-1].cached; memmove(pool+k+1,pool+k, sizeof(pool[0])*(EVPOOL_SIZE-k-1)); pool[k].cached = cached; } else { sds cached = pool[0].cached; /* Save SDS before overwriting. */ if (pool[0].key != pool[0].cached) sdsfree(pool[0].key); memmove(pool,pool+1,sizeof(pool[0])*k); pool[k].cached = cached; /* some code */ }由于篇幅原因,我截取了部分代码,通过这段代码我们可以看到,Redis首先是采样了一部分key,这里采样数量maxmemory_samples通常是5,我们也可以自己设置,采样数量越大,结果就越接近LRU算法的结果,带来的影响是性能随之变差。 采样之后我们需要获得每个key的空闲时间,然后将其填充到“过期池”中的指定位置。这里“过期池”是按照空闲时间从小到大排序的,也就是说,idle大大key排在最右边。 填充完“过期池”之后,会从后向前获取到最适合清理的key。 / Go backward from best to worst element to evict. /for (k = EVPOOL_SIZE-1; k &gt;= 0; k--) { if (pool[k].key == NULL) continue; bestdbid = pool[k].dbid; if (server.maxmemory_policy &amp; MAXMEMORY_FLAG_ALLKEYS) { de = dictFind(server.db[pool[k].dbid].dict, pool[k].key); } else { de = dictFind(server.db[pool[k].dbid].expires, pool[k].key); } / some code / if (de) { bestkey = dictGetKey(de); break; }}找到需要删除的key后,就需要根据设置清理策略进行同步/异步清理。 if (server.lazyfree_lazy_eviction) dbAsyncDelete(db,keyobj);else dbSyncDelete(db,keyobj)最后记下本次清理的空间大小,用来在循环条件判断是否要继续清理。 delta -= (long long) zmalloc_used_memory();mem_freed += delta;清理策略最后我们来看一下Redis支持的几种清理策略 noeviction:不会继续处理写请求(DEL可以继续处理)。allkeys-lru:对所有key的近似LRUvolatile-lru:使用近似LRU算法淘汰设置了过期时间的keyallkeys-random:从所有key中随机淘汰一些keyvolatile-random:对所有设置了过期时间的key随机淘汰volatile-ttl:淘汰有效期最短的一部分keyRedis4.0开始支持了LFU策略,和LRU类似,它分为两种: volatile-lfu:使用LFU算法淘汰设置了过期时间的keyallkeys-lfu:从全部key中进行淘汰,使用LFU写在最后现在我知道了Redis在内存达到上限时做了哪些事了。以后出问题时也就不会只检查自己的db了。 关于这次事故的后续处理,我首先是让业务同学回滚了代码,然后让他们使用一个单独的Redis,这样业务再出现类似问题就不会影响到我们的帐号服务了,整体的影响范围也会变得更加可控。 原文地址https://www.cnblogs.com/Jackeyzhe/p/12616624.html

常见排序算法总结分析之选择排序与归并排序-C#实现

常见排序算法总结分析之选择排序与归并排序-C#实现本篇文章对选择排序中的简单选择排序与堆排序,以及常用的归并排序做一个总结分析。 常见排序算法总结分析之交换排序与插入排序-C#实现是排序算法总结系列的首篇文章,包含了一些概念的介绍以及交换排序(冒泡与快速排序)和插入排序(直接插入与希尔排序)的总结,感兴趣的同学可以先去看一下。 选择排序选择排序主要包括两种排序算法,分别是简单选择排序和堆排序 简单选择排序基本思想每一趟在待排序列中选出最小(或最大)的元素,依次放在已排好序的元素序列后面(或前面),直至全部的元素排完为止。 简单选择排序也被称为直接选择排序。首先在待排序列中选出最小的元素,将它与第一个位置上的元素交换。然后选出次小的元素,将它与第二个位置上的元素交换。以此类推,直至所有元素排成递增序列为止。 选择排序是对整体的选择。只有在确定了最小数(或最大数)的前提下才进行交换, 大大减少了交换的次数。 复杂度与稳定性与优缺点空间复杂度:O(1)时间复杂度:O(n2)最好情况:O(n2),此时不发生交换,但仍需进行比较最坏情况:O(n2)稳定性:不稳定,因为在将最小或最大元素替换到前面时,可能将排在前面的相等元素交换到后面去优点:交换数据的次数已知(n - 1)次缺点:不稳定,比较的次数多算法实现public void SimpleSelectionSort(int[] array){ for(int i = 0; i &lt; array.Length - 1; i ++){ int index = i; for(int j = index + 1; j &lt; array.Length; j ++){ if(array[j] &lt; array[index]){ index = j; if(index != i) Swap(array, i, index); public void Swap(int[] array, int i, int j){ int temp = array[i]; array[i] = array[j]; array[j] = temp; }【算法解读】 初始时无序区为整个待排序列。算法内层循环遍历整个无序区的所有元素,找到其中最小的元素,用index记录其下标位置。然后将找到的最小元素与无序区的首元素进行交换,这样就完成了一趟选择排序,此时序列的首元素处于有序区中,剩下的元素处于无序区中。重复上面的操作,继续查找无序区中的最小元素,并将找到的最小元素和无序区首元素进行交换。直至完成所有排序。 【举个栗子】 对于待排序列3,1,4,2首先将序列首元素3的索引0保存在index中,从元素1开始与index位置上的元素(此时是3)进行比较,1&lt;3,则index保存元素1的索引。继续将index位置上的元素(此时是1)与元素4比较,4&gt;1,继续与2比较,1&lt;2,不需要改变。没有需要再比较的元素了,此时将index记录的索引位置上的元素3和无序区首元素进行交换。则完成一趟选择排序.,序列为1,3,4,2。有序区为1,无序区为3,4,2。继续下一趟排序,将找到的无序区最小元素,和无序区首元素进行交换。这一趟选择排序结束后,序列为1,2,4,3。有序区为1,2,无序区为4,3。重复上述操作直到完成排序。 堆排序堆排序是借助堆来实现的选择排序。 什么是堆呢?堆是满足下列性质的数列{ R1, R2, R3, R4, ..., Rn }: Ri&lt;=R2i且Ri&lt;=R2i + 1或者是Ri&gt;=R2i且Ri&gt;=R2i + 1,前者称为小顶堆,后者称为大顶堆。 例如小顶堆:{10,34,24,85,47,33,53},位置i(i从1开始)上的元素小于2i位置上的元素,且小于2i+1位置上的元素。绘制成堆的形状,如下图所示,可以发现每个堆的堆顶元素(每个二叉树的根节点)均小于其左子节点(2i)与右子节点(2i + 1)元素。 对于小顶堆而言,整个堆的堆顶元素便是整个序列的最小值,大顶堆同理。 基本思想对待排序列的所有元素,首先将它们排成满足大顶堆或小顶堆的定义的序列,常称为建堆。建堆完成后堆顶元素便是最大或最小元素。然后将堆顶元素移出(比如移动到序列尾部),再对剩余的元素进行再建堆,常称为重新调整成堆,即可通过堆顶元素得到次大(次小)元素,如此反复进行,直到完成排序为止。 实现堆排序有两个关键步骤,建堆和调整堆如何建堆:首先将待排序列画成一颗完全二叉树,然后再把得到的完全二叉树转换成堆。从最后一个有孩子节点的节点(这样可以构成一颗有孩子的树,根节点才能够向下渗透)开始(对于数组而言就是下标为n / 2 - 1的节点)),依次将所有以该节点为根的二叉树调整成堆,即先调整子树,再调整父树,当这个过程持续到整颗二叉树树的根节点时,待排序列就被调整成了堆,即建堆完成。 如何调整堆:假设被调整的节点为A,它的左孩子为B,右孩子为C。那么当A开始进行堆调整时,根据上面建堆的方式,以B和以C为根的二叉树都已经为堆。如果节点A的值大于B和C的值(以大顶堆为例),那么以A为根的二叉树已经是堆。如果A节点的值小于B节点或C节点的值,那么节点A与值最大的那个孩子节点变换位置。此时需要继续将A与和它交换的那个孩子的两个孩子节点进行比较,以此类推,直到节点A向下渗透到适当的位置为止。 如果要从小到大排序,则使用大顶堆,如果要从大到小排序,则使用小顶堆。原因是堆顶元素需要交换到序列尾部 复杂度与稳定性与优缺点空间复杂度:O(1)时间复杂度:O(nlog2n)最好情况:O(nlog2n)最坏情况:O(nlog2n),它的最坏情况接近于平均性能稳定性:不稳定优点:在最坏情况下性能优于快速排序。由于在直接选择排序的基础上利用了比较结果形成堆。效率提高很大。缺点:不稳定,初始建堆所需比较次数较多,因此记录数较少时不宜采用算法实现public void HeapSort(int[] array){ // 建堆 for(int i = array.Length / 2 - 1; i &gt;= 0; i --){ BuildHeap(array, i, array.Length - 1); // 调整堆 for(int i = array.Length - 1; i &gt; 0; i --){ Swap(array, 0, i); BuildHeap(array, 0, i - 1); public void BuildHeap(int[] array, int left, int right){ int target = array[left]; for(int i = 2 * left + 1; i &lt;= right; i = 2 * i + 1){ if(i &lt; right &amp;&amp; array[i + 1] &gt; array[i]){ i ++; if(target &gt;= array[i]){ break; array[left] = array[i]; left = i; array[left] = target; public void Swap(int[] array, int i, int j){ int temp = array[i]; array[i] = array[j]; array[j] = temp; 【算法解读】 算法也是按照先建堆再调整堆的步骤执行的。第一个for循环,从n / 2 - 1节点开始依次通过调用BuildHeap来形成堆,最终完成整个待排序列建堆。第二个for循环,利用之前已经建好的大顶堆(首元素为最大值),将首元素交换到序列末尾。然后将剩下的元素,再调整堆,再次获得大顶堆(首元素为次大值),将其首元素交换到倒数第二个位置,以此类推。 算法的关键点在于堆调整BuildHeap方法。该方法调整的节点为left位置的元素(称其为目标元素)。该元素的左右孩子分别是2 left + 1,2 left + 2。若目标元素大于等于它的的两个孩子,则已经是大顶堆,不需要调整了。否则,目标元素和两个孩子中的较大值交换(对应代码array[left] = array[i];,即向下渗透),并将left设置为目标元素交换后所在的位置,重复上述操作,直到目标元素渗透到适当的位置。 【举个栗子】 对于待排序列1,4,3,2首先为了便于理解,我们可以将其画成二叉树: 转换方法是将待排序列的元素,从上到下,从左到右,依次填入到二叉树的节点中。开始建堆。本例中实际上只需要调整节点1,所以以调整节点1为例:过程如下图 节点1作为目标元素,先找到其左右孩子(4和3)的较大值4,即比较目标元素1和4,1&lt;4,则交换位置。目标元素1渗透到元素4位置(4被交换到1的位置)。在此位置上继续寻找,其左右孩子,此时只有一个左孩子,元素2,与目标元素做比较,1&lt;2,则1渗透到2的位置,此时目标元素1已经向下渗透到最终位置。建堆成功,序列为4,2,3,1。然后通过大顶堆,得到首元素最大值4,并将其移动到序列尾部。去掉元素4后,再次建堆,重复上述操作,完成排序。 归并排序基本思想所谓归并是指,把两个或两个以上的待排序列合并起来,形成一个新的有序序列。2-路归并是指,将两个有序序列合并成为一个有序序列。2-路归并排序的基本思想是,对于长度为n的无序序列来说,归并排序把它看成是由n个只包括一个元素的有序序列组成,然后进行两两归并,最后形成包含n个元素的有序序列即先将待排序列通过递归拆解成子序列,然后再对已经排好序的子序列进行合并 复杂度与稳定性与优缺点空间复杂度:O(n),因为在实现过程中用到了一个临时序列来暂存归并过程中的中间结果时间复杂度:O(nlog2n)最好情况:O(nlog2n)最坏情况:O(nlog2n)稳定性:稳定优点:稳定,若采用单链表作为存储结构,可实现就地排序,不需要额外空间缺点:需要O(n)的额外空间算法实现public void MergeSort(int[] array){ MergeSortImpl(array, 0, array.Length - 1); public void MergeSortImpl(int[] array, int left, int right){ if(left &gt;= right) return; int middle = (left + right) / 2; MergeSortImpl(array, left, middle); MergeSortImpl(array, middle + 1, right); Merge(array, left, middle, right); // 合并两个子序列public void Merge(int[] array, int left, int middle, int right){ int[] temp = new int[right - left + 1]; int index = 0, lindex = left, rindex = middle + 1; while(lindex &lt;= middle &amp;&amp; rindex &lt;= right){ if(array[rindex] &lt; array[lindex]){ temp[index ++] = array[rindex ++]; }else{ temp[index ++] = array[lindex ++]; while(lindex &lt;= middle){ temp[index ++] = array[lindex ++]; while(rindex &lt;= right){ temp[index ++] = array[rindex ++]; while(--index &gt;= 0){ array[left + index] = temp[index]; }【算法解读】 算法首先通过递归,不断将待排序列划分成两个子序列,子序列再划分成两个子序列,直到每个子序列只含有一个元素(对应代码:if(left &gt;= right) return;),然后对每对子序列进行合并。合并子序列是通过Merge方法实现,首先定义了一个临时的辅助空间,长度是两个子序列之和。然后逐个比较两个子序列中的元素,元素较小的先放入辅助空间中。若两个子序列长度不同,则必定有一个子序列有元素未放入辅助空间,因此分别对左边子序列和右边子序列中的剩余元素做了处理。最后,两个子序列的合并结果都存在于辅助空间中,将辅助空间中的有序序列替换到原始序列的对应位置上。 【举个栗子】 对于待排序列1,4,3,2第一次递归,middle = 1,将待排序列分成(1,4),(3,2)。继续对每个子序列划分子序列。对于序列(1,4,),(3,2),middle都是0,即分别被划分成(1)(4),(3)(2)直到每个子部分只含有一个元素。然后开始合并,合并(1)(4)得到有序序列(1,4) ,合并(3)(2)得到有序序列(2,3)。再次合并(1,4)(2,3),得到最终有序序列(1,2,3,4) 更多本篇文章算法的源码都放在了GitHub上,感兴趣的同学可以点击这里查看更多算法的总结与代码实现(不仅仅是排序算法),可以查看GitHub仓库Algorithm了解 可以发现本篇所汇总的算法,时间复杂度最低也就是O(nlog2n),包括上一篇汇总讲到的交换排序和插入排序也是同样的结果。其实,基于比较的排序算法,时间复杂度的下界就是O(nlog2n),下一篇会总结分析可以突破下界O(nlog2n),达到O(n)的排序算法。敬请期待。 作者:iwiniwin出处:http://www.cnblogs.com/iwiniwin/

nginx request body读取流程详解

nginx request body读取流程详解前面的文章中我们分别讲解了nginx是如何读取请求行和请求头数据的,在读取完请求头之后,nginx并不会直接读取请求体,而是直接进入http模块的11个阶段开始处理请求的数据。在这个过程中,如果当前请求匹配的location配置了proxy_pass,那么就会在NGX_HTTP_CONTENT_PHASE阶段读取客户端发送来的request body数据,以转发给上游服务器。本文主要是对nginx是如何读取客户端的数据进行讲解的。 request body读取入口nginx读取数据是通过ngx_http_read_client_request_body()进行的,如下是该方法的源码: 接收http请求的包体*/ ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler) { size_t preread; ssize_t size; ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_request_body_t *rb; ngx_http_core_loc_conf_t *clcf; r-&gt;main-&gt;count++; // 如果当前请求是子请求,或者已经接收过包体,或者需要忽略包体,则直接调用post_handler()方法,然后返回 if (r != r-&gt;main || r-&gt;request_body || r-&gt;discard_body) { r-&gt;request_body_no_buffering = 0; post_handler(r); return NGX_OK; // 主要是向用户发送100 continue以期待获取更多数据 if (ngx_http_test_expect(r) != NGX_OK) { rc = NGX_HTTP_INTERNAL_SERVER_ERROR; goto done; // 申请ngx_http_request_body_t的内存 rb = ngx_pcalloc(r-&gt;pool, sizeof(ngx_http_request_body_t)); if (rb == NULL) { rc = NGX_HTTP_INTERNAL_SERVER_ERROR; goto done; rb-&gt;rest = -1; rb-&gt;post_handler = post_handler; r-&gt;request_body = rb; // 如果content-length小于0,并且不是大文件,则直接回调post_handler(),并且返回 if (r-&gt;headers_in.content_length_n &lt; 0 &amp;&amp; !r-&gt;headers_in.chunked) { r-&gt;request_body_no_buffering = 0; post_handler(r); return NGX_OK; // preread表示缓冲区还有多少数据未处理。这里这么计算的原因在于,r-&gt;header_in中关于请求行和header的 // 数据都已经处理了,剩下的部分如果还有数据,必然是请求包体的,也即当前方法需要处理的部分 preread = r-&gt;header_in-&gt;last - r-&gt;header_in-&gt;pos; // 已经读取到了部分数据 if (preread) { out.buf = r-&gt;header_in; out.next = NULL; // 将out中的数据尝试写入到临时文件中 rc = ngx_http_request_body_filter(r, &amp;out); if (rc != NGX_OK) { goto done; // 对处理的数据长度进行累加 r-&gt;request_length += preread - (r-&gt;header_in-&gt;last - r-&gt;header_in-&gt;pos); // 这里是判断r-&gt;header_in中是否已经读取到的数据长度大于剩余需要读取的长度 if (!r-&gt;headers_in.chunked &amp;&amp; rb-&gt;rest &gt; 0 &amp;&amp; rb-&gt;rest &lt;= (off_t) (r-&gt;header_in-&gt;end - r-&gt;header_in-&gt;last)) { b = ngx_calloc_buf(r-&gt;pool); if (b == NULL) { rc = NGX_HTTP_INTERNAL_SERVER_ERROR; goto done; b-&gt;temporary = 1; b-&gt;start = r-&gt;header_in-&gt;pos; b-&gt;pos = r-&gt;header_in-&gt;pos; b-&gt;last = r-&gt;header_in-&gt;last; b-&gt;end = r-&gt;header_in-&gt;end; rb-&gt;buf = b; r-&gt;read_event_handler = ngx_http_read_client_request_body_handler; r-&gt;write_event_handler = ngx_http_request_empty_handler; rc = ngx_http_do_read_client_request_body(r); goto done; } else { if (ngx_http_request_body_filter(r, NULL) != NGX_OK) { rc = NGX_HTTP_INTERNAL_SERVER_ERROR; goto done; // rb-&gt;rest为0说明没有包体数据,或者包体数据已经读取完毕 if (rb-&gt;rest == 0) { r-&gt;request_body_no_buffering = 0; post_handler(r); return NGX_OK; // 这里rb-&gt;rest肯定是大于0的,因而如果其小于0,说明读取包体异常了 if (rb-&gt;rest &lt; 0) { ngx_log_error(NGX_LOG_ALERT, r-&gt;connection-&gt;log, 0, "negative request body rest"); rc = NGX_HTTP_INTERNAL_SERVER_ERROR; goto done; clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); size = clcf-&gt;client_body_buffer_size; size += size &gt;&gt; 2; // 计算读取数据缓冲区的大小 if (!r-&gt;headers_in.chunked &amp;&amp; rb-&gt;rest &lt; size) { size = (ssize_t) rb-&gt;rest; if (r-&gt;request_body_in_single_buf) { size += preread; } else { size = clcf-&gt;client_body_buffer_size; // 创建读取数据的缓冲区 rb-&gt;buf = ngx_create_temp_buf(r-&gt;pool, size); if (rb-&gt;buf == NULL) { rc = NGX_HTTP_INTERNAL_SERVER_ERROR; goto done; r-&gt;read_event_handler = ngx_http_read_client_request_body_handler; r-&gt;write_event_handler = ngx_http_request_empty_handler; rc = ngx_http_do_read_client_request_body(r); done: if (r-&gt;request_body_no_buffering &amp;&amp; (rc == NGX_OK || rc == NGX_AGAIN)) { if (rc == NGX_OK) { r-&gt;request_body_no_buffering = 0; } else { r-&gt;reading_body = 1; r-&gt;read_event_handler = ngx_http_block_reading; post_handler(r); if (rc &gt;= NGX_HTTP_SPECIAL_RESPONSE) { r-&gt;main-&gt;count--; return rc;}上述读取request body的流程主要分为如下几个步骤: 判断当前请求如果是子请求,或者已经调用当前方法读取过body,或者被设置为需要忽略body,则直接返回;申请ngx_http_request_body_t结构体的内存,这个结构体的作用是存储当前读取到的body数据的;判断当前请求的Content-Length是否为0,是则不进行body的读取;判断当前请求的读取缓冲区中是否已经读取了一部分的body数据,如果已经读取了,则调用ngx_http_request_body_filter()方法将已经读取到的数据存储到临时文件中,否则只是调用ngx_http_request_body_filter()方法计算当前还剩余多少数据未读取,计算的到的值存储在rb-&gt;rest中。这里可能存在部分已经读取的数据的原因在于,前面在读取请求行和header数据的时候可能已经多读取了一部分数据,而这部分数据就是body数据;判断当前剩余需要读取的数据长度为0,则直接返回;申请存储body数据的缓冲区;将当前请求的read_event_handler设置为ngx_http_read_client_request_body_handler()方法,而write_event_handler设置为ngx_http_request_empty_handler()方法。在ngx_http_read_client_request_body_handler()内部,其会调用ngx_http_do_read_client_request_body()方法以执行真正的请求体读取过程。这里的ngx_http_request_empty_handler()方法是一个空方法,由于当前正处于读取数据阶段,因而意外触发的写事件不需要处理;调用ngx_http_do_read_client_request_body()以执行真正的body读取过程。这里需要说明的是,第七步中将read_event_handler设置为ngx_http_read_client_request_body_handler(),这个read_event_handler()方法的调用方式在前面讲解nginx如何驱动http模块的11个阶段的文章中已经进行了介绍。简而言之,进入http模块的11个阶段的流程之后,如果触发了读事件,那么就会调用read_event_handler()方法,这里设置了之后,每次触发了读事件就会调用ngx_http_read_client_request_body_handler()方法,从而触发body数据的继续读取流程。 上面的流程中,最主要的两个方法是ngx_http_request_body_filter()和ngx_http_do_read_client_request_body()方法,第一个方法在缓冲区数据满了的时候会将数据写入到缓冲区中,第二个方法则用于读取客户端的body数据。 存储缓冲区数据至临时文件存储缓冲区数据到临时文件的方法为ngx_http_request_body_filter()方法,如下是该方法的源码: static ngx_int_t ngx_http_request_body_filter(ngx_http_request_t r, ngx_chain_t in) { if (r-&gt;headers_in.chunked) { return ngx_http_request_body_chunked_filter(r, in); } else { // 我们这里主要讲解小块body的处理方式 return ngx_http_request_body_length_filter(r, in); 这里主要是将in中的数据写入到临时文件中*/ static ngx_int_t ngx_http_request_body_length_filter(ngx_http_request_t r, ngx_chain_t in) { size_t size; ngx_int_t rc; ngx_buf_t *b; ngx_chain_t cl, tl, out, *ll; ngx_http_request_body_t *rb; rb = r-&gt;request_body; // rest为-1表示还未读取过任何包体数据,因而将rest设置为content_length_n的值 if (rb-&gt;rest == -1) { rb-&gt;rest = r-&gt;headers_in.content_length_n; out = NULL; ll = &amp;out; for (cl = in; cl; cl = cl-&gt;next) { if (rb-&gt;rest == 0) { break; tl = ngx_chain_get_free_buf(r-&gt;pool, &amp;rb-&gt;free); if (tl == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; b = tl-&gt;buf; ngx_memzero(b, sizeof(ngx_buf_t)); b-&gt;temporary = 1; b-&gt;tag = (ngx_buf_tag_t) &amp;ngx_http_read_client_request_body; b-&gt;start = cl-&gt;buf-&gt;pos; b-&gt;pos = cl-&gt;buf-&gt;pos; b-&gt;last = cl-&gt;buf-&gt;last; b-&gt;end = cl-&gt;buf-&gt;end; b-&gt;flush = r-&gt;request_body_no_buffering; size = cl-&gt;buf-&gt;last - cl-&gt;buf-&gt;pos; if ((off_t) size &lt; rb-&gt;rest) { cl-&gt;buf-&gt;pos = cl-&gt;buf-&gt;last; rb-&gt;rest -= size; } else { cl-&gt;buf-&gt;pos += (size_t) rb-&gt;rest; rb-&gt;rest = 0; b-&gt;last = cl-&gt;buf-&gt;pos; b-&gt;last_buf = 1; *ll = tl; ll = &amp;tl-&gt;next; // 这里的ngx_http_top_request_body_filter指向的是ngx_http_request_body_save_filter方法, // 这里主要是检查现有的缓冲区是否写满了,如果满了,则将缓冲区的数据写入到临时文件中 rc = ngx_http_top_request_body_filter(r, out); // 释放busy中ngx_buf_t链表占用的缓冲区,不过不会释放其tag为ngx_http_read_client_request_body的 // 缓冲区,而会将这些缓冲区添加到free链表的头部 ngx_chain_update_chains(r-&gt;pool, &amp;rb-&gt;free, &amp;rb-&gt;busy, &amp;out, (ngx_buf_tag_t) &amp;ngx_http_read_client_request_body); return rc;}上述方法主要完成了如下几部分工作: 遍历out链表,依次将其存储的数据长度从rb-&gt;rest中扣除;调用ngx_http_top_request_body_filter()判断如果缓冲区中没有剩余空间,则将数据存储到临时文件中;调用ngx_chain_update_chains()方法将rb-&gt;busy中需要释放的空间释放到rb-&gt;free中;这里我们主要看看ngx_http_top_request_body_filter()方法是如何存储数据到临时文件的,这个方法实际上指向的是ngx_http_request_body_save_filter()方法,如下是该方法的源码: 这里主要是判断是否还有剩余的可用空间,如果没有可用空间,则会将现有的数据写入到包体中*/ ngx_int_t ngx_http_request_body_save_filter(ngx_http_request_t r, ngx_chain_t in) { ngx_buf_t *b; ngx_chain_t *cl; ngx_http_request_body_t *rb; rb = r-&gt;request_body; // 将in的数据拼接到rb-&gt;bufs的末尾 if (ngx_chain_add_copy(r-&gt;pool, &amp;rb-&gt;bufs, in) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; if (r-&gt;request_body_no_buffering) { return NGX_OK; if (rb-&gt;rest &gt; 0) { // 这里rest大于0,说明还有数据未接收,但是这里的buf-&gt;last等于buf-&gt;end,说明缓冲区中没有剩余可用于 // 存储数据的空间了,因而这里调用ngx_http_write_request_body()方法将已经接收到的包体写入到临时 // 文件中 if (rb-&gt;buf &amp;&amp; rb-&gt;buf-&gt;last == rb-&gt;buf-&gt;end &amp;&amp; ngx_http_write_request_body(r) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; return NGX_OK; // 走到这里,说明客户端的包体数据已经接收完毕了 if (rb-&gt;temp_file || r-&gt;request_body_in_file_only) { if (ngx_http_write_request_body(r) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; if (rb-&gt;temp_file-&gt;file.offset != 0) { cl = ngx_chain_get_free_buf(r-&gt;pool, &amp;rb-&gt;free); if (cl == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; b = cl-&gt;buf; ngx_memzero(b, sizeof(ngx_buf_t)); // 标记数据已经写入到文件中了 b-&gt;in_file = 1; b-&gt;file_last = rb-&gt;temp_file-&gt;file.offset; b-&gt;file = &amp;rb-&gt;temp_file-&gt;file; rb-&gt;bufs = cl; return NGX_OK;}在ngx_http_request_body_save_filter()方法中,其首先会将in中的数据拷贝到rb-&gt;bufs缓冲区中。然后会判断当前缓冲区是否还有剩余空间,如果没有,则调用ngx_http_write_request_body()方法将数据写入到临时文件中,并且更新表征当前request body的rb结构体中与文件相关的属性。关于ngx_http_write_request_body()是如何将数据写入到临时文件的,其逻辑比较简单,这里不再赘述。 读取request body数据前面我们讲到,nginx读取request body是通过ngx_http_do_read_client_request_body()方法进行的,如下是该方法的源码: static ngx_int_t ngx_http_do_read_client_request_body(ngx_http_request_t *r) { off_t rest; size_t size; ssize_t n; ngx_int_t rc; ngx_chain_t out; ngx_connection_t *c; ngx_http_request_body_t *rb; ngx_http_core_loc_conf_t *clcf; c = r-&gt;connection; rb = r-&gt;request_body; for (;;) { for (;;) { // 判断rb-&gt;buf中是否还有可用的空间 if (rb-&gt;buf-&gt;last == rb-&gt;buf-&gt;end) { if (rb-&gt;buf-&gt;pos != rb-&gt;buf-&gt;last) { out.buf = rb-&gt;buf; out.next = NULL; // 将数据写入到临时文件中 rc = ngx_http_request_body_filter(r, &amp;out); if (rc != NGX_OK) { return rc; } else { rc = ngx_http_request_body_filter(r, NULL); if (rc != NGX_OK) { return rc; if (rb-&gt;busy != NULL) { if (r-&gt;request_body_no_buffering) { if (c-&gt;read-&gt;timer_set) { ngx_del_timer(c-&gt;read); if (ngx_handle_read_event(c-&gt;read, 0) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; return NGX_AGAIN; return NGX_HTTP_INTERNAL_SERVER_ERROR; // 更新rb-&gt;buf的指针数据 rb-&gt;buf-&gt;pos = rb-&gt;buf-&gt;start; rb-&gt;buf-&gt;last = rb-&gt;buf-&gt;start; // size表示rb-&gt;buf中剩余的可用空间 size = rb-&gt;buf-&gt;end - rb-&gt;buf-&gt;last; // 计算新的未读取的数据长度 rest = rb-&gt;rest - (rb-&gt;buf-&gt;last - rb-&gt;buf-&gt;pos); if ((off_t) size &gt; rest) { size = (size_t) rest; // 从连接的句柄中读取size大小的数据到rb-&gt;buf-&gt;last位置 n = c-&gt;recv(c, rb-&gt;buf-&gt;last, size); // NGX_AGAIN表示还未读取完 if (n == NGX_AGAIN) { break; // n等于0表示客户端关闭了连接 if (n == 0) { ngx_log_error(NGX_LOG_INFO, c-&gt;log, 0, "client prematurely closed connection"); // 如果读取异常,则返回BAD_REQUEST if (n == 0 || n == NGX_ERROR) { c-&gt;error = 1; return NGX_HTTP_BAD_REQUEST; rb-&gt;buf-&gt;last += n; r-&gt;request_length += n; // n等于rest表示剩余的数据都读取完了,此时将数据写入到临时文件中 if (n == rest) { out.buf = rb-&gt;buf; out.next = NULL; rc = ngx_http_request_body_filter(r, &amp;out); if (rc != NGX_OK) { return rc; // 如果rb-&gt;rest为0,则退出当前内层循环 if (rb-&gt;rest == 0) { break; // 如果当前rb-&gt;buf中还有可用空间,则退出当前内层循环 if (rb-&gt;buf-&gt;last &lt; rb-&gt;buf-&gt;end) { break; // 如果没有需要读取的数据,则跳出外层循环 if (rb-&gt;rest == 0) { break; // 如果连接句柄上没有可以读取的数据,则继续将读取事件添加到事件调度器中,并且监听连接句柄上的读取事件 if (!c-&gt;read-&gt;ready) { if (r-&gt;request_body_no_buffering &amp;&amp; rb-&gt;buf-&gt;pos != rb-&gt;buf-&gt;last) { /* pass buffer to request body filter chain */ out.buf = rb-&gt;buf; out.next = NULL; rc = ngx_http_request_body_filter(r, &amp;out); if (rc != NGX_OK) { return rc; clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); ngx_add_timer(c-&gt;read, clcf-&gt;client_body_timeout); if (ngx_handle_read_event(c-&gt;read, 0) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; return NGX_AGAIN; // 走到这里,说明数据读取完毕了,因而这里会删除读取事件,并且将读取事件的处理方法设置为一个空方法 if (c-&gt;read-&gt;timer_set) { ngx_del_timer(c-&gt;read); if (!r-&gt;request_body_no_buffering) { r-&gt;read_event_handler = ngx_http_block_reading; rb-&gt;post_handler(r); return NGX_OK;}这里ngx_http_do_read_client_request_body()方法使用了一个双重无限for循环,对于内层循环,其主要分为四部分的工作: 检查当前读取缓冲区是否有剩余的空间,如果没有,则还是调用ngx_http_request_body_filter()方法将数据写入到临时文件中;计算下一次应该读取的数据长度;调用c-&gt;recv()方法读取当前请求句柄上的数据,返回值如果大于0,则表示此次读取到的数据大小,如果等于0,则表示客户端断开了连接,如果为NGX_AGAIN,则表示需要再次长度读取,如果为NGX_ERROR,则表示读取异常了;根据上一步调用的返回值以判断应该作何处理,对于NGX_AGAIN,直接跳出当前循环,对于0和NGX_ERROR,直接给客户端返回400响应码,对于大于0的情况,则更新当前缓冲区的相关属性,并且调用ngx_http_request_body_filter()尝试将满了的数据存储到临时文件中。本质上来讲,内层循环如果读取到了数据,并且还有数据需要读取,那么其是不会跳出循环的,并且循环会一直尝试读取数据,这样做的优点在于,客户端传送的body数据一般都比较大,读取事件可能会经常触发,通过循环读取可以尽快的读取到连接句柄上接收到的数据。对于内层循环返回NGX_AGAIN时,表示此次读取的数据长度为0,此时可能句柄比较空闲,因而此时会break到外层循环(当然,还有其他的情况也都可以break到外层循环)。外层循环中,只要当前剩余需要读取的数据不为0,并且连接事件不是ready状态,则会将当前事件再次添加到事件框架中,以等待下次驱动读事件读取剩余的数据。 小结本文首先对nginx读取request body数据的入口进行了讲解,然后讲解了读取的整体流程,接着着重讲解了读取过程中临时文件的存储方式,以及读取数据的具体细节。 原文地址https://my.oschina.net/zhangxufeng/blog/3215381

升级Kubernetes 1.18前,你不得不知的9件事

升级Kubernetes 1.18前,你不得不知的9件事昨天Kubernetes最新版本v1.18已经发布,其包含了38项功能增强,其中15项为稳定版功能、11项beta版功能以及12项alpha版功能。在本文中,我们将探索其中一些功能,希望能帮助你决定是否需要升级。那么,我们现在开始吧! 将Service Account Token作为通用身份验证方法Kubernetes使用service account来验证集群内的服务。例如,如果你想要一个pod来管理其他Kubernetes资源,如Deployment或者Service,你可以与Service Account相关联并创建必要的角色和角色绑定。Kubernetes Service Account(KSA)发送JSON Web Tokens(JWT)到API server以验证它们。这使API server成为service account身份验证的唯一来源。 那么,如果实体需要针对集群外的其他服务进行身份验证,该怎么办?为了使用其KSA,外部身份验证器必须联系API server以验证请求。但是,API server不应公开访问。因为这使你可以使用其他身份验证系统进行验证,这会增加复杂性。即使第三方服务位于可以访问API server的集群中,也会增加负载。 于是在Kubernetes 1.18中增加了一个功能(#1393),该功能使API server提供OpenID Connect发现文档,该文档包含Token的公共密钥以及其他元数据。OIDC身份验证器可以使用此数据对token进行身份验证,而不必先引用API server。 为特定Pod配置HPA速率Horizontal Pod Autoscaler(HPA)可以使你的Kubernetes集群对高/低流量自动做出反应。通过HPA,你可以指示controller根据CPU峰值、其他指标或者应用程序提供的指标来创建更多的Pod。为了优化成本,HPA会在不需要多余的Pod(例如不再有高负载时)时将其终止。而HPA是以配置的速率增加/减少Pod,以避免在不稳定时间内产生/破坏波动的pod。但是,目前该功能仅在集群级别可以配置。在典型的微服务应用程序中,你经常拥有一些比其他服务更重要的服务。假设你在Kubernetes上托管一个Web应用程序,该程序执行以下任务: 响应最终客户的请求(前端)处理客户端提供的数据(包括执行大量CPU操作,例如map-reduce)。处理不太重要的数据(例如,存档、清理等)从上述内容明显看出,任务1要求pod更快地扩展,以便应用程序可以快速有效地处理增加的客户端流量。此外,它们应该非常缓慢地缩小规模,以防再次出现流量高峰。 任务2需要pod也可以非常快地扩展以响应增加的数据量。在关键任务应用程序中,不应延迟数据处理。但是,它们也应该非常迅速地缩减规模,因为一旦不再需要,它们会消耗大量地资源,而无法将这些资源用于其他服务。 由于它们的重要性,我们可以在一定程度上容忍属于任务1和2的pod对误报做出响应。毕竟,浪费一些资源比失去客户要更好。 服务于任务3的pod不需要特别地安排,因为它们按照常规的方式扩展和缩小即可。 在Kubernetes 1.18中提供了功能(#853),允许通过HPA行为字段配置弹性伸缩。在行为字段下的scaleUp或scaleDown部分中分别指定了用于按比例缩放的行为。 在集群级别定义偶数Pod扩展规则在Kubernetes 1.16中首次引入Even Pod Spreading,它可以确保以最高的可用性和资源利用率的方式在可用区上(如果你使用的是多区域集群)调度Pod。该功能通过指定topologySpreadConstraints来发挥作用,通过搜索具有相同topologyKey标签的节点来识别区域。具有相同topologyKey标签的节点属于同一区域。该设置将pod均匀分配到不同区域中。但是,它的缺点是必须在Pod级别应用此设置。没有配置参数的pod将不会在故障域之间分布。 这一功能(#895)允许你为不提供任何topologySpreadConstraints的Pod定义default spreading constraints。已定义此设置的Pod将覆盖全局级别。 在Windows上支持Containerd 1.3当我们谈论“Kubernetes”时,我们几乎第一时间想到的是Linux。即使在教程、大部分的书籍和文献中也普遍将Linux视为运行Kubernetes的事实上的操作系统。 然而,Microsoft Windows已经采取相应的措施来支持Kubernetes在Windows Server系列产品上运行。这些措施包括添加对Containerd运行时版本1.3的支持。Windows Server2019包含更新的主机容器服务(HCS v2),其功能是增强了对容器管理的控制,这可能会改善Kubernetes API的兼容性。但是,当前的Docker版本(EE18.09)还未与Windows HCSv2兼容,只有Containerd可以使用。使用Containerd运行时可以在Windows操作系统和Kubernetes之间实现更好的兼容性,也将提供更多功能。该功能(#1001)引入了对Windows的Containerd 1.3版本的支持,并将其作为容器运行时的接口(CRI)。 在同一集群中支持RuntimeClass和多个Windows版本的标签既然Microsoft Windows正在积极支持Kubernetes的各种功能,因此今天能够看到在Linux和Windows节点上运行的混合集群并不稀奇。早在Kubernetes 1.12就引入了RuntimeClass,而Kubernetes 1.14引入了主要的增强功能。它可以让你选择容器运行时,并且其上运行特定的pod。现在,在Kubernetes 1.18中,RuntimeClass支持Windows节点。所以你可以选择节点来调度应仅在Windows上运行的Pod,该节点运行特定的Windows构建。 跳过Volume所有权更改默认情况下,将volume安装到Kubernetes集群中的容器时,该volume内的所有文件和目录所有权都将更改为提供的fsGroup值。这样做的原因是使该volume可由fsGroup读取和写入。但是,这种行为在某些情况下并不是那么受欢迎。例如: 某些应用程序(如数据库)对文件许可权和所有权修改很敏感。装入volume后,这些应用程序可能会停止启动。当volume很大(&gt; 1TB)或者其中包含的文件和目录的数量很大时,chown和chmod操作可能会太长。在某些情况下,启动Pod时可能会导致超时。该功能(#695)提供了FSGroupChangePolicy参数,将该参数设置为“always”,将保持当前行为。而设置为OnRootMismatch时,它只会在顶级目录与预期的fsGroup值不匹配时更改volume权限。 允许Secret和ConfigMap不可变在Kubernetes早期,我们就已经使用ConfigMap来将配置数据注入到我们的容器中。如果数据十分敏感,那么则会使用Secret。将数据呈现给容器最常见的方式是通过挂载一个包含数据的文件。但是,当对ConfigMap或Secret进行更改时,此更改将会立刻传递到安装了该配置文件的所有pod。也许这并不是将更改应用于正在运行的集群的最佳方式。因为如果新配置有问题,我们将面临停止运行应用程序的风险。 修改Deployment时,将通过滚动更新策略应用更改,在该策略中,将创建新的Pod,而旧的Pod在删除之前仍然有作用。该策略可以确保如果新的Pod无法启动,则该应用程序仍将在旧的Pod上运行。ConfigMap和Secret也采用了类似的方法,它们通过在不可变字段中启用不可变性。当对象不可变时,API将拒绝对其进行任何更改。为了修改对象,你必须删除它并重新创建它,同事还要重新创建使用它的所有容器。使用Deployment滚动更新,可以在删除旧的Pod之前确保新的pod在新的配置中正常工作,以避免由于配置更改错误而导致应用程序中断。 另外,将ConfigMaps和Secrets设置为不可变,可以节省API server不必定期轮询它们的更改。通过启用ImmutableEmphemeralVolumes功能门,可以在Kubernetes 1.18中启用该功能(#1412)。然后在ConfigMap或Secret资源文件中将不可变值设置为true,对资源键所做的任何更改都将被拒绝,从而保护集群不受意外的坏更新的影响。 使用Kubectl调试为用户提供更多故障排除功能作为Kubernetes用户,当你需要查看正在运行的Pod时,你将受到kubectl exec和kubectl port-forward的限制。而在Kubernetes 1.18中,你还可以使用kubectl debug命令。该命令允许你执行以下操作: 将临时容器部署到正在运行的Pod。临时容器声明周期短,它们通常包含必要的调试工具。由于它们是在同一pod中启动的,因此它们可以访问具有相同网络和文件系统的其他容器。这在极大程度上可以帮助你解决问题或跟踪问题。 使用修改后的PodSpec重新就地启动Pod。这使你可以进行诸如更改容器的源镜像或权限之类的操作。 你甚至可以在主机命名空间中启动特权容器。这使你可以对节点问题进行故障排除。 总 结Kubernetes是一项不断变化的技术,每个版本中都添加了越来越多的功能。在本文中,我们简要讨论了Kubernetes 1.18中一些最有趣的新功能。但是,毋庸置疑,升级Kubernetes集群并不是一个容易做出的决定。希望文章里对一些功能的介绍,可以给予你一些有意义的参考。 本文来自Rancher Labs 转载地址https://www.cnblogs.com/rancherlabs/p/12579866.html

【Springboot】用Prometheus+Grafana监控Springboot应用

【Springboot】用Prometheus+Grafana监控Springboot应用1 简介项目越做越发觉得,任何一个系统上线,运维监控都太重要了。关于Springboot微服务的监控,之前写过【Springboot】用Springboot Admin监控你的微服务应用,这个方案可以实时监控并提供告警提醒功能,但不能记录历史数据,无法查看过去1小时或过去1天等运维情况。本文介绍Prometheus + Grafana的方法监控Springboot 2.X,实现美观漂亮的数据可视化。 2 PrometheusPrometheus是一套优秀的开源的监控、报警和时间序列数据库组合系统,在现在最常见的Kubernetes容器管理系统中,通常会搭配Prometheus进行监控。 2.1 引入到Springboot将Prometheus引入依赖如下: io.micrometermicrometer-registry-prometheus对于Springboot,要开启Actuator,并打开对应的Endpoint: management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=prometheus启动Springboot后,可以通过下面URL看能不能正确获取到监控数据: localhost:8080/actuator/prometheus获取数据成功,说明Springboot能正常提供监控数据。 2.2 Docker方式使用为了方便,使用Docker启动Prometheus: 拉取docker镜像 docker pull prom/prometheus准备配置文件prometheus.yml: scrape_configs: 可随意指定 job_name: 'spring' # 多久采集一次数据 scrape_interval: 15s # 采集时的超时时间 scrape_timeout: 10s # 采集的路径 metrics_path: '/actuator/prometheus' # 采集服务的地址,设置成Springboot应用所在服务器的具体地址 static_configs: targets: ['hostname:9000','hostname:8080']启动docker实例: 端口为9090,指定配置文件 docker run -d -p 9090:9090 -v ~/temp/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus --config.file=/etc/prometheus/prometheus.yml2.3 测试与查看成功启动后,就可以打开网页查看了,并且能图形化展示,URL为http://localhost:9090/。 如上图所示,打开网页后,随便选取一个对应的监控指标与参数,点击Execute就可以查看了。 3 GrafanaGrafana是一个开源的度量分析与可视化套件,纯JavaScript开发的前端工具,通过访问库(如InfluxDB),展示自定义报表、显示图表等。它的UI十分灵活,有丰富的插件和模板,功能强大。一般用在时序数据的监控方面。 3.1 Docker安装与启动 docker pull grafana/grafana docker run -d -p 3000:3000 grafana/grafana启动成功后,访问http://localhost:3000 检查是否成功,初始管理员账号密码为admin/admin。 3.2 配置数据源Grafana展示数据,则需要配置对应的数据源,本文中配置之前安装启用的Prometheus数据源,具体配置如下图所示: 需要注意的是Access要选Browser模式,否则无法正常获取数据。配置完成后,点击Save &amp; Test即可。 3.3 模板套用能够获取数据后,就可以自定义数据可视化展示了。但如果自己一条指标一条指标的加,就会很麻烦。实际上,Grafana提供了许多优秀的模板,可以网页https://grafana.com/grafana/dashboards 查找。 本文使用Spring Boot 2.1 Statistics模板,导入方法如下: 点击+号 --&gt; Import --&gt; 输入模板链接或ID --&gt; 点击Load。 成功导入后,就能监控数据了,而且,界面真的很好看: 4 总结本文介绍了如何使用Prometheus + Grafana监控Springboot应用,实际上,Prometheus + Grafana十分强大,值得花时间好好研究。 本文例子中软件版本信息如下: springboot.version=2.2.5micrometer-registry-prometheus=1.3.5prometheus.version=2.16grafana.version=6.7.0-beta1原文地址https://www.cnblogs.com/larrydpk/p/12563497.html

RestFul API 统一格式返回 + 全局异常处理

RestFul API 统一格式返回 + 全局异常处理一、背景在分布式、微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式。前端和后端进行交互,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。 所以统一接口的返回值,保证接口返回值的幂等性很重要,本文主要介绍博主当前使用的结果集。 二、统一格式设计2.1 统一结果的一般形式示例:{ # 是否响应成功 success: true, # 响应状态码 code: 200, # 响应数据 data: Object # 返回错误信息 message: "", }2.2 结果类枚举public enum ResultCodeEnum { /*** 通用部分 100 - 599***/ // 成功请求 SUCCESS(200, "successful"), // 重定向 REDIRECT(301, "redirect"), // 资源未找到 NOT_FOUND(404, "not found"), // 服务器错误 SERVER_ERROR(500,"server error"), /*** 这里可以根据不同模块用不同的区级分开错误码,例如: ***/ // 1000~1999 区间表示用户模块错误 // 2000~2999 区间表示订单模块错误 // 3000~3999 区间表示商品模块错误 // 。。。 * 响应状态码 private Integer code; * 响应信息 private String message; ResultCodeEnum(Integer code, String msg) { this.code = code; this.message = msg; public Integer getCode() { return code; public String getMessage() { return message; }code:响应状态码一般小伙伴们是在开发的时候需要什么,就添加什么。但是,为了规范,我们应当参考HTTP请求返回的状态码。 code区间 类型 含义1** 100-199 信息 服务器接收到请求,需要请求者继续执行操作2** 200-299 成功 请求被成功接收并处理3** 300-399 重定向 需要进一步的操作以完成请求4** 400-499 客户端错误 请求包含语法错误或无法完成请求5** 500-599 服务器错误 服务器在处理的时候发生错误常见的HTTP状态码: 200 - 请求成功;301 - 资源(网页等)被永久转移到其它URL;404 - 请求的资源(网页等)不存在;500 - 内部服务器错误。message:错误信息在发生错误时,如何友好的进行提示? 根据code 给予对应的错误码定位;把错误描述记录到message中,便于接口调用者更详细的了解错误。2.3 统一结果类public class HttpResult implements Serializable { * 是否响应成功 private Boolean success; * 响应状态码 private Integer code; * 响应数据 private T data; * 错误信息 private String message; // 构造器开始 * 无参构造器(构造器私有,外部不可以直接创建) private HttpResult() { this.code = 200; this.success = true; * 有参构造器 * @param obj private HttpResult(T obj) { this.code = 200; this.data = obj; this.success = true; * 有参构造器 * @param resultCode private HttpResult(ResultCodeEnum resultCode) { this.success = false; this.code = resultCode.getCode(); this.message = resultCode.getMessage(); // 构造器结束 * 通用返回成功(没有返回结果) * @param &lt;T&gt; * @return public static&lt;T&gt; HttpResult&lt;T&gt; success(){ return new HttpResult(); * 返回成功(有返回结果) * @param data * @param &lt;T&gt; * @return public static&lt;T&gt; HttpResult&lt;T&gt; success(T data){ return new HttpResult&lt;T&gt;(data); * 通用返回失败 * @param resultCode * @param &lt;T&gt; * @return public static&lt;T&gt; HttpResult&lt;T&gt; failure(ResultCodeEnum resultCode){ return new HttpResult&lt;T&gt;(resultCode); public Boolean getSuccess() { return success; public void setSuccess(Boolean success) { this.success = success; public Integer getCode() { return code; public void setCode(Integer code) { this.code = code; public T getData() { return data; public void setData(T data) { this.data = data; public String getMessage() { return message; public void setMessage(String message) { this.message = message; @Override public String toString() { return "HttpResult{" + "success=" + success + ", code=" + code + ", data=" + data + ", message='" + message + '\'' + 构造器私有,外部不可以直接创建;只可以调用统一返回类的静态方法返回对象;success 是一个Boolean 值,通过这个值,可以直接观察到该次请求是否成功;data 表示响应数据,用于请求成功后,返回客户端需要的数据。三、测试及总结3.1 简单的接口测试@RestController@RequestMapping("/httpRest")@Api(tags = "统一结果测试")public class HttpRestController { @ApiOperation(value = "通用返回成功(没有返回结果)", httpMethod = "GET") @GetMapping("/success") public HttpResult success(){ return HttpResult.success(); @ApiOperation(value = "返回成功(有返回结果)", httpMethod = "GET") @GetMapping("/successWithData") public HttpResult successWithData(){ return HttpResult.success("风尘博客"); @ApiOperation(value = "通用返回失败", httpMethod = "GET") @GetMapping("/failure") public HttpResult failure(){ return HttpResult.failure(ResultCodeEnum.NOT_FOUND); }这里 Swagger以及SpringMVC的配置就没贴出来了,详见Github 示例代码。 3.2 返回结果http://localhost:8080/swagger-ui.html#/ { "code": 200, "success": true}{ "code": 200, "data": "风尘博客", "success": true}{ "code": 404, "message": "not found", "success": false}四、全局异常处理使用统一返回结果时,还有一种情况,就是程序的报错是由于运行时异常导致的结果,有些异常是我们在业务中抛出的,有些是无法提前预知。 因此,我们需要定义一个统一的全局异常,在Controller捕获所有异常,并且做适当处理,并作为一种结果返回。 4.1 设计思路:自定一个异常类(如:TokenVerificationException),捕获针对项目或业务的异常;使用@ExceptionHandler注解捕获自定义异常和通用异常;使用@ControllerAdvice集成@ExceptionHandler的方法到一个类中;异常的对象信息补充到统一结果枚举中;4.2 自定义异常public class TokenVerificationException extends RuntimeException { * 错误码 protected Integer code; protected String msg; public Integer getCode() { return code; public String getMsg() { return msg; public void setMsg(String msg) { this.msg = msg; * 有参构造器,返回码在枚举类中,这里可以指定错误信息 * @param msg public TokenVerificationException(String msg) { super(msg); }4.3 统一异常处理器@ControllerAdvice注解是一种作用于控制层的切面通知(Advice),能够将通用的@ExceptionHandler、@InitBinder和@ModelAttributes方法收集到一个类型,并应用到所有控制器上。 @RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler { * 异常捕获 * @param e 捕获的异常 * @return 封装的返回对象 @ExceptionHandler(Exception.class) public HttpResult handlerException(Exception e) { ResultCodeEnum resultCodeEnum; // 自定义异常 if (e instanceof TokenVerificationException) { resultCodeEnum = ResultCodeEnum.TOKEN_VERIFICATION_ERROR; resultCodeEnum.setMessage(getConstraintViolationErrMsg(e)); log.error("tokenVerificationException:{}", resultCodeEnum.getMessage()); }else { // 其他异常,当我们定义了多个异常时,这里可以增加判断和记录 resultCodeEnum = ResultCodeEnum.SERVER_ERROR; resultCodeEnum.setMessage(e.getMessage()); log.error("common exception:{}", JSON.toJSONString(e)); return HttpResult.failure(resultCodeEnum); * 获取错误信息 * @param ex * @return private String getConstraintViolationErrMsg(Exception ex) { // validTest1.id: id必须为正数 // validTest1.id: id必须为正数, validTest1.name: 长度必须在有效范围内 String message = ex.getMessage(); try { int startIdx = message.indexOf(": "); if (startIdx &lt; 0) { startIdx = 0; int endIdx = message.indexOf(", "); if (endIdx &lt; 0) { endIdx = message.length(); message = message.substring(startIdx, endIdx); return message; } catch (Throwable throwable) { log.info("ex caught", throwable); return message; }说明我使用的是@RestControllerAdvice ,等同于@ControllerAdvice + @ResponseBody错误枚举类这里省略了,详见Github代码。五、测试及总结5.1 测试接口@RestController@RequestMapping("/exception")@Api(tags = "异常测试接口")public class ExceptionRestController { @ApiOperation(value = "业务异常(token 异常)", httpMethod = "GET") @GetMapping("/token") public HttpResult token() { // 模拟业务层抛出 token 异常 throw new TokenVerificationException("token 已经过期"); @ApiOperation(value = "其他异常", httpMethod = "GET") @GetMapping("/errorException") public HttpResult errorException() { //这里故意造成一个其他异常,并且不进行处理 Integer.parseInt("abc123"); return HttpResult.success(); }5.2 返回结果http://localhost:8080/swagger-ui.html#/ { "code": 500, "message": "For input string: "abc123"", "success": false}{ "code": 4000, "message": "token 已经过期", "success": false}5.3 小结@RestControllerAdvice和@ExceptionHandler会捕获所有Rest接口的异常并封装成我们定义的HttpResult的结果集返回,但是:处理不了拦截器里的异常 六、总结没有哪一种方案是适用于各种情况的,如:分页情况,还可以增加返回分页结果的静态方案,具体实现,这里就不展示了。所以,适合自己的,具有一定可读性都是很好的,欢迎持不同意见的大佬给出意见建议。 6.1 示例代码Github 示例代码 6.2 技术交流风尘博客风尘博客-掘金风尘博客-博客园Github原文地址https://www.cnblogs.com/vandusty/p/12557551.html

SpringCloud之Hystrix服务降级入门全攻略

SpringCloud之Hystrix服务降级入门全攻略理论知识Hystrix是什么?Hystrix是由Netflix开源的一个服务隔离组件,通过服务隔离来避免由于依赖延迟、异常,引起资源耗尽导致系统不可用的解决方案。这说的有点儿太官方了,它的功能主要有以下三个: 服务降级​ SpringCloud是通过HTTP Rest的方式在“微服务”之间进行调用的,所以每一个“微服务”都是一个web项目。既然它是一个web项目,它就就有可能会发生错误,这个错误有可能是服务器内存不足、客户端传参错误、网络问题等,也有可能是人为的(这个就是服务熔断)。也就是说,会因为一些原因从而不能给调用者返回正确的信息。 ​ 对于我们目前的单个SpringBoot项目来说,我们使用Ajax等一些方式调用接口时,如果服务器发生错误,我们在前端就会对这个错误进行处理。有可能是重试调用接口,或者给用户一个友好的提示,比如“服务繁忙,稍后再试”啥的。 ​ 但是在分布式系统中,同样也会发生一些“错误”,而且在多个服务之间调用时,如果不能对这些“错误”进行友好的处理,就会导致我们整个项目瘫痪,这是万万不能发生的。所以Hystrix利用服务降级来很好的解决了这个问题。这个其实就类似于我们的try-catch这样的机制,发生错误了,我就执行catch中的代码。 ​ 通过服务降级,能保证在某个或某些服务出问题的时间,不会导致整个项目出现问题,避免级联故障,从而来提高分布式系统的弹性。 服务熔断​ 建设先看下边的服务降级代码,将整个服务降级的代码部分全部看完,再来看下边这段理论,你一定会茅塞顿开的。 ​ Hystrix意为“断路器”,就和我们生活中的保险丝,开关一个道理。 ​ 当我们给整个服务配置了服务降级后,如果服务提供者发生了错误后,就会调用降级后的方法来保证程序的运行。但是呢?有一个问题,调用者并不知道它调用的这个服务出错了,就会在业务发生的时候一直调用,然后服务会一直报错,然后去调用降级方法。好比下图中: ​ 它们的对话如下: ​ Client:我要调用你的方法A ​ Server:不行,我报错了。你调用降级方法吧,你的我的都行! ​ Client:哎呀,服务器报错了,那我就调用一下降级方法吧。 ​ 过了一会儿。。。。。。 ​ Client:我要调用你的方法A ​ Server:刚才不是说了吗?我报错了。你调用降级方法吧,你的我的都行! ​ Client:哎呀,服务器报错了,那我就调用一下降级方法吧。 ​ 又过了一会儿。。。。。。 ​ Client:我要调用你的方法A ​ Server:没完了是吧?我说过我报错了,你去调用这个降级方法啊。非要让我的代码又运行一次? 以上的对话说明了一个问题,当服务端发生了错误后,客户端会调用降级方法。但是,当有一个新的访问时,客户端会一直调用服务端,让服务端运行一些明知会报错的代码。这能不能避免啊,我知道我错了,你访问我的时候,就直接去访问降级方法,不要再让我执行错的代码。 ​ 这就是服务熔断,就好比我们家中的保险丝。当检测到家中的用电负荷过大时,就断开一些用电器,来保证其他的可用。在分布式系统中,就是调用一个系统时,在一定时间内,这个服务发生的错误次数达到一定的值时, 我们就打开这个断路器,不让调用过去,而是让他直接去调用降级方法。再过一段时间后,当一次调用时,发现这个服务通了,就将这个断路器改为“半开”状态,让调用一个一个的慢慢过去,如果一直没有发生错误,就将这个断路器关闭,让所有的服务全部通过。 服务限流​ 服务限流就容易理解多了,顾名思义,这是对访问的流量进行限制,就比如上边的场景,我们还可能通过服务限流的方法来解决高并发以及秒杀等问题。主流的限流算法主要有:漏桶算法和令牌算法 开始码代码吧​ 不贴代码,说这么多有什么用?这不是耍流氓吗? 先创建一个我们需要的几个项目:模块名称 代码中项目名称 备注Eureka注册中心 eureka-alone-7000 测试期间,使用一个注册中心而不是集群客户端(消费者,服务调用者) hystrix-consumer-80 使用Feign或OpenFeign进行服务调用服务端(提供者,服务提供者) hystrix-provider-8001 ​ 这三个项目的创建代码略(项目代码地址) 在客户端和服务端都加入Hystrix的依赖(当然是在哪端进行服务降级就在哪端使用) &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt; &lt;artifactId&gt;spring-cloud-starter-hystrix&lt;/artifactId&gt; 服务降级服务降级有两种解决思路:可以分别从服务调用者和服务提供者进行服务降级,也就是进行错误的“兜底” 从服务提供者方进行服务降级我们先在提供者方的下列方法模拟一个“响应超时错误”。 * 这个方法会造成服务调用超时的错误 * 其实本身体不是错误,而是服务响应时间超过了我们要求的时间,就认为它错了 * @param id * @return public String timeOutError(Integer id){ return "服务调用超时"; 我们就给它定义一个错误回调方法,加上如下注解: 这个方法会造成服务调用超时的错误 其实本身体不是错误,而是服务响应时间超过了我们要求的时间,就认为它错了 @param id @return*/ @HystrixCommand(fallbackMethod = "TimeOutErrorHandler",commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000") })public String timeOutError(Integer id){ try { //我们让这个方法休眠5秒,所以一定会发生错误,也就会调用下边的fallbakcMethod方法 TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); return "服务正常调用"+id; 这个就是当上边方法的“兜底”方法*/ public String TimeOutErrorHandler(Integer id) { return "对不起,系统处理超时"+id; }上边这个注解要注意三点: fallCallbackMethod中的这个参数就是“兜底”方法fallCallbackMethod中的这个方法的声明要和本方法一致commandProperties属性中可以写多个@HystrixProperty注解,其中的name和value就是配置对应的属性,上例中的这个就是配置响应超时最后在主启动类上加上这个注解 @SpringBootApplication@EnableEurekaClient //本服务启动后会自动注册进eureka服务中@EnableCircuitBreakerpublic class ProviderAppication_8001 { public static void main(String[] args) { SpringApplication.run(ProviderAppication_8001.class, args); }这个我们是在服务提供者方面进行的错误处理,所以对服务调用者不做任何处理,启动三个项目(consumer,provder,eureka)。然后访问http://localhost/consumer/hello/999,理论上是要返回服务调用正常999,但是呢,由于我们人为造成了超时错误,所以就一定会返回fallback中的对不起,系统处理超时999,而且这个返回是会在3秒后。 ​ 如果你觉得上边这个超时的错误演示很麻烦,可以直接在方法中写一个运行时错误,比如:int i = 10/0;也会进行fallbackMethod的调用。之所以要用这个超时配置,就是为了让你知道Hystrix可以对什么样的错误进行fallback,它的更多配置参考https://github.com/Netflix/Hystrix/wiki/Configuration 2.从服务提供者方进行服务降级和在服务提供方进行服务降级相比,在服务调用方(客户端、消费者)进行服务降级是更常用的方法。这两者相比,前者是要让服务提供者对自己可发生的错误进行“预处理”,这样,一定要保证调用者访问到我才会调用这个“兜底”方法。但是,大家想一下,如果我这个服务宕机了呢?客户端根本就调用不到我,它怎么可能接收到我的“兜底”方法呢?所以,在客户端进行服务降级是更常用的方法。 一个小疑问,如果我在客户端和服务端都进行了服务降级,是都会调用?还是先调用哪个?自己想喽,稍微动动你聪明的小脑袋。 为了不和上一个项目的代码冲突,我将上边这个@Service给注掉(也就是让Spring来管理它),从而用另外一个接口的实现,下边是我们新的serive类@Servicepublic class OrignService implements IExampleService { * 不用这个做演示,就空实现 @Override public String timeOutError(Integer id) { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); return "服务正常调用"+id; * 不发生错误的正确方法 @Override public String correct(Integer id) { return "访问正常,服务端没有进行任何错误"+id; }在主启动类上添加如下注解:@EnableHystrix //注意这个和服务端的注解是不一样的在application.yml中开户feign对Hystrix的支持feign: hystrix: enabled:true 将之前在provider项目中的@HystrixCommond放在feign的接口中 3.改进下解决方案以上的两种方案看似可行, 但是,实际呢?心想,这是一个合格程序员应该做的事吗?每个接口我们都要写一个fallback方法?然后和我们的业务代码要写在一起?就好的“低耦合,高内聚”呢?第一种解决方案,就是使用@DefaultProperties在整个Controller类上,顾名思义,就是给它一个默认的“兜底”方法,就不用每一个需要降级的方法进行设置fallbackMethod了,我们只需要加上@HystrixCommand好了。这个方法太过简单,不做代码演示,在文末的代码中专门写了注释第二种解决方法:我们在客户端不是通过Feign调用的吗?是有一个Feign的本地接口类,我们直接对这个类进行设置就好了。直接上代码。@Component//@FeignClient(value = "hystrix-provider") //这是之前的调用@FeignClient(value = "hystrix-provider",fallback = ProviderServiceImpl.class) //这回使用了Hystrix的服务降级public interface IProviderService { @GetMapping("provider/hello/{id}") public String hello(@PathVariable("id") Integer id); }@Componentpublic class ProviderServiceImpl implements IProviderService { @Override public String hello(Integer id) { return "调用远程服务错误了"; }以上两种方法的对比:第一种和我们的业务类进行了耦合,而且如果要对每个方法进行fallback,就要多写一个方法,代码太过臃肿。但是,它提供了一个DefaultProperties注解,可以提供默认的方法,这个后者是没有的。这种方法适合直接使用Ribbon结合RestTemplate进行调用的方法第二种提供了一个Feign接口的实现类来处理服务降级问题,将所有的fallback方法写到了一起,和我们的业务代码完全解耦了。对比第一个,我们可以定义一个统一的方法来实现DefalutPropeties。这种方法适合Feign作为客户端的调用,比较推荐这种。服务熔断​ 请再回去看一下上边的关于服务熔断的理论知识,我相信你一定能看懂。当启用服务降级时,会默认启用服务熔断机制,我们只需要对一些参数进行配置就可以了,就是在上边的@HystrixCommand中的一些属性,比如: @HystrixCommand(fallbackMethod = "TimeOutErrorHandler",commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000"), @HystrixProperty(name="circuitBreaker.enabled",value="true"),//开户断路器 @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="20"),//请求次数的峰值 @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="10000"),//检测错误次数的时间范围 @HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="60")//请求失败率达到多少比例后会打开断路器 })这些配置可以在https://github.com/Netflix/Hystrix/wiki/Configuration了解,也可以打开查看HystrixCommandProperties类中的属性(使用idea一搜索就有),全部都有默认配置 原文地址https://www.cnblogs.com/Lyn4ever/p/12528913.html

Spring MVC启动流程分析

Spring MVC启动流程分析本文是Spring MVC系列博客的第一篇,后续会汇总成贴子。 Spring MVC是Spring系列框架中使用频率最高的部分。不管是Spring Boot还是传统的Spring项目,只要是Web项目都会使用到Spring MVC部分。因此程序员一定要熟练掌握MVC部分。本篇博客就简要分析下Spring MVC的启动流程,帮助我们更好的理解这个框架。 为什么要写这篇博客#Spring的MVC框架已经出来很久了,网上介绍这部分的博客有很多很多,而且很多肯定比我自己写的好,那我还为什么要写这篇博客呢。一方面我觉得博客是对自己学习过程的一个记录,另一方面写博客的过程能加深自己对相关技术的理解,也方便以后自己回顾总结。 Spring MVC简介#什么是Spring MVC#要回答这个问题,我们先要说说MVC。MVC是一种设计模式,这种设计模式建议将一个请求由M(Module)、V(View)、C(controller)三个部分进行处理。请求先经过controller,controller调用其他服务层得到Module,最后将Module数据渲染成试图(View)返回客户端。Spring MVC是Spring生态圈的一个组件,一个遵守MVC设计模式的WEB MVC框架。这个框架可以和Spring无缝整合,上手简单,易于扩展。 解决什么问题#通常我们将一个J2EE项目项目分为WEB层、业务逻辑层和DAO层。Spring MVC解决的是WEB层的编码问题。Spring MVC作为一个框架,抽象了很多通用代码,简化了WEB层的编码,并且支持多种模板技术。我们不需要像以前那样:每个controller都对应编写一个Servlet,请求JSP页面返回给前台。 优缺点#用的比较多的MVC框架有Struts2和Spring MVC。两者之间的对比: 最大的一个区别就是Struts2完全脱离了Servlet容器,而SpringMVC是基于Servlet容器的;Spring MVC的核心控制器是Servlet,而Struts2是Filter;Spring MVC默认每个Controller是单列,而Struts2每次请求都会初始化一个Action;Spring MVC配置较简单,而Struts2的配置更多还是基于XML的配置。总的来说,Spring MVC比较简单,学习成本低,和Spring能无缝集成。在企业中也得到越来越多的应用。所以个人比较建议在项目中使用Spring MVC。 启动流程分析#PS:本文的分析还是基于传统的Tomcat项目分析,因为这个是基础。现在非常流行的Spring Boot项目中的启动流程后续也会写文章分析。其实原理差不多... 要分析Spring MVC的启动过程,要从它的启动配置说起。一般会在Tomcat的 Web.xml中配置了一个ContextLoaderListener和一个DispatcherServlet。其实ContextLoaderListener是可以不配,这样的话Spring会将所有的bean放入DispatcherServlet初始化的上下文容器中管理。这边我们就拿常规的配置方式说明Spring MVC的启动过程。(PS:Spring Boot启动过程已经不使用Web.xml) Copybr&gt; "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" &gt;

手写Promise原理

我的promise能实现什么?1:解决回调地狱,实现异步2:可以链式调用,可以嵌套调用3:有等待态到成功态的方法,有等待态到失败态的方法4:可以衍生出周边的方法,如Promise.resolve(),Promise.reject(),Promise.prototype.then(),Promise.prototype.catch(),Promise.all() // 所有的完成 可以根据自己的需求调节自己的promise let PromiseA = require('./PromiseA'); const promiseA = new PromiseA((resolve, reject) =&gt; { resolve(new PromiseA((resolve,reject)=&gt;{ setTimeout(()=&gt;{ resolve(100) },1000) promiseA.then(data=&gt;{ console.log(data) 下面开始实现promise,首先创造三个常量,等待,成功,失败 const PENDING = 'PENDING'; // 等待状态const RESOLVED = 'RESOLVED'; // 成功状态const REJECTED = 'REJECTED'; // 失败状态然后创造一个promiseA类,里面有constructor,then方法,catch方法。这里的catch其实就是失败的then,即then(null,(err)=&gt;{...}) class PromiseA { constructor(){...} then(){...}    catch(err){        return this.then(null,err)    }} 我们重点关注constructor和then,先来看constructor。这里promiseA默认的状态是等待态,成功的值value默认为undefined,失败的值reason默认为undefined。这里的onResolvedCallbacks和onRejectedCallbacks是一个发布订阅的数组,我们先不管。然后有resolve方法,reject方法 还有Promise自带一个executor执行器,就是传进来的参数。会立即执行 。 但有可能出错。所以用try,catch包住。 executor里有俩个参数,就是resolve和reject。就是promise传进来参数的俩个resolve,reject方法. constructor(executor) { this.status = PENDING; // 默认等待状态 this.value = undefined; this.reason = undefined; this.onResolvedCallbacks = []; this.onRejectedCallbacks = []; let resolve = (value) =&gt; { if(value instanceof PromiseA){ value.then(resolve,reject) return if (this.status === PENDING) { this.value = value; this.status = RESOLVED; this.onResolvedCallbacks.forEach(fn =&gt; fn()); let reject = (reason) =&gt; { if (this.status === PENDING) { this.reason = reason; this.status = REJECTED; this.onRejectedCallbacks.forEach(fn =&gt; fn()); try { executor(resolve, reject); } catch (e) { reject(e) 然后我们在看看then,then里面传进来俩个参数,其实就是俩个方法。一个成功的回调,一个失败的回调。我们重点看一下,三个状态的执行,即status的走向。如果是resolve,即执行成功的回调onFulfilled。如果是reject,即执行失败的回调onRejected。如果是等待,即执行一个发布订阅的模式,发布订阅,其实就是,我先将成功的回调或者的失败的回调各自放入对应的数组,即是上面我们跳过的俩个数组onResolvedCallbacks和onRejectedCallbacks 。然后,当状态改变为resolve或者reject的时候,即遍历执行对应的回调函数。至此异步就实现了,回调地狱解决。这个异步解决就是靠发布订阅模式来解决的。 then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled:v=&gt;v onRejected = typeof onRejected === 'function' ? onRejected:e=&gt;{throw e} let promise2 = new PromiseA((resolve, reject) =&gt; { if (this.status === RESOLVED) { setTimeout(() =&gt; { try { let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e) if (this.status === REJECTED) { setTimeout(() =&gt; { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e) if (this.status === PENDING) { this.onResolvedCallbacks.push(() =&gt; { setTimeout(() =&gt; { try { let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(err); this.onRejectedCallbacks.push(() =&gt; { setTimeout(() =&gt; { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(err); return promise2; 接下来我们继续实现链式调用,既是一个promiseA.then的结果返回的e是promise2的then的data。 一个成功的结果返回给下一个then作为参数。 let promise2 = promiseA.then(e=&gt;{ return e })promise2.then(data=&gt;{ console.log(data,'123') 那么继续实现,还是上面的函数。这里我们重点观察这个promise2和setTimeout和resolvePromise. 我们先说promise2,这里的promise2,其实是一个新的promise.也就是说promise的链式调用靠的就是返回一个新的promise.这里把之前的三种状态包起来,目的就是可以让里面得到的结果,获取给promise2的resolve和reject。有人可能会说,那么promise2的resolve或者reject要还是promise怎么办?这里我们就要用到resolvePromise方法来判断了。所以我们将成功的回调函数resolve换成resolvePromise方法来执行。这里我们要明白,resolve是执行成功的回调函数。不管状态是成功还是失败,如果执行成功都是走向resolve。所以resolve和reject的状态如果执行成功都是走向resolve。 let promise2 = new PromiseA((resolve, reject) =&gt; { if (this.status === RESOLVED) { setTimeout(() =&gt; { try { let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e) if (this.status === REJECTED) { setTimeout(() =&gt; { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e) if (this.status === PENDING) { this.onResolvedCallbacks.push(() =&gt; { setTimeout(() =&gt; { try { let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(err); this.onRejectedCallbacks.push(() =&gt; { setTimeout(() =&gt; { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(err); return promise2; 然后我们在看resolvePromise方法,走到这里,说明是执行成功的回调函数了。传进去的参数有promise2,x,promise2的resolve,promise2的reject。首先promise2是一个new promise。 这样传进去参数是会报错的。因为执行到这一步,promise2还没有生成。所以是会报错的。 所以我们加一个setTimeout包住它,这样就可以等promise2生成完在执行。这里的setTimeout涉及到了事件循环,也就是宏任务和微任务的部分。js执行机制是先执行主线程,然后执行微任务队列,然后进行渲染,然后在执行宏任务队列。宏任务执行完,如果宏任务里还包着js任务,就继续循环反复。直到所有任务执行完成。这里的setTimeout是宏任务, resolvePromise(promise2, x, resolve, reject);刚刚说到setTimeout,这里贴上一段代码。这里的newPromise是在setTimeout前执行的。 console.log(1); setTimeout(() =&gt; { console.log("我是定时器,延迟0S执行的");}, 0); new Promise((resolve, reject) =&gt; { console.log("new Promise是同步任务里面的宏任务"); resolve("我是then里面的参数,promise里面的then方法是宏任务里面的微任务");}).then(data =&gt; { console.log(data);}); console.log(2); 好的参数都传进去了,接下来我们看resolvePromise的具体方法。就是一个判断回调函数x是不是promise,如果是就在循环拆开。直到不是为止。如果是普通值的话就可以直接返回了。至此,所以的promise库就实现完了。至于后面的all和其他周边方法就是语法糖了。主要核心部分掌握了,后面的周边方法就不算什么了。 function resolvePromise(promise2, x, resolve, reject) { if (promise2 === x) { return reject(new TypeError('返回的promise和当前promise不能是同一个对象哦,会嵌入死循环')) if ((typeof x === 'object' &amp;&amp; x !== null) || typeof x === 'function') { try { let then = x.then; if (typeof then === 'function') { then.call(x,y=&gt; { resolvePromise(promise2, y, resolve, reject) },r=&gt; { reject(r) } else { resolve(x) } catch (err) { reject(err) } else { resolve(x); 至于resolve方法里判断,我们来看看。其实也是一个递归。判断PromiseA里的resolve里面是不是promise,一直拆开。跟上面的方法类似。 let resolve = (value) =&gt; { if(value instanceof PromiseA){ value.then(resolve,reject) return if (this.status === PENDING) { this.value = value; this.status = RESOLVED; this.onResolvedCallbacks.forEach(fn =&gt; fn()); 而至于这俩行代码,就是解决一个不断向jquery那样then的情况。如果是函数的话,将自己的参数作为结果返回。传递给下一个then。 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled:v=&gt;v onRejected = typeof onRejected === 'function' ? onRejected:e=&gt;{throw e} promiseA.then().then().then().then().then()原文地址https://www.cnblogs.com/At867604340/p/12486678.html

django实战商城项目注册业务实现

django实战商城项目注册业务实现设计到的前端知识项目的前端页面使用vue来实现局部刷新,通过数据的双向绑定实现与用户的交互,下面来看一下需求,在用户输入内容后,前端需要做一些简单的规则校验,我们希望在在用户输入后能够实时检测,如果有错误能够在输入框的下方显示出来。 &lt;label&gt;用户名:&lt;/label&gt; &lt;input type="text" name="username" id="user_name"&gt; &lt;span class="error_tip"&gt;请输入5-20个字符的用户&lt;/span&gt; &lt;label&gt;密码:&lt;/label&gt; &lt;input type="password" name="password" id="pwd"&gt; &lt;span class="error_tip"&gt;请输入8-20位的密码&lt;/span&gt; 上面是一个用户和密码的输入框,当用户输入完用户名以后,光标离开输入框,能够实时的检测输入内容的正确性,当输入有问题的时候,在输入框的下方显示错误信息。v-model实现数据的双向绑定,v-on进行事件绑定,v-show是控制dom显示与否,下面是加入vue后的部分代码 &lt;label&gt;用户名:&lt;/label&gt; &lt;input type="text" name="username" id="user_name" v-model="username" @blur="check_username"&gt; &lt;span class="error_tip" v-show="error_name"&gt;[[error_name_message]]&lt;/span&gt; &lt;label&gt;密码:&lt;/label&gt; &lt;input type="password" name="password" id="pwd" v-model="password" @blur="check_password"&gt; &lt;span class="error_tip" v-show="error_password"&gt;请输入8-20位的密码&lt;/span&gt; 用户输入的用户名和username变量绑定,光标消失触发绑定时间check_username,通过v-show绑定到布尔值变量error_name,来控制是否显示字符串变量error_name_message,其他的输入框都类似这种操作。注册业务实现前端注册业务逻辑注册表单代码: {{ csrf_input }} &lt;ul&gt; &lt;li&gt; &lt;label&gt;用户名:&lt;/label&gt; &lt;input type="text" name="username" id="user_name" v-model="username" @blur="check_username"&gt; &lt;span class="error_tip" v-show="error_name"&gt;[[error_name_message]]&lt;/span&gt; &lt;/li&gt; &lt;li&gt; &lt;label&gt;密码:&lt;/label&gt; &lt;input type="password" name="password" id="pwd" v-model="password" @blur="check_password"&gt; &lt;span class="error_tip" v-show="error_password"&gt;请输入8-20位的密码&lt;/span&gt; &lt;/li&gt; &lt;li&gt; &lt;label&gt;确认密码:&lt;/label&gt; &lt;input type="password" v-model="password2" @blur="check_password2" name="password2" id="cpwd"&gt; &lt;span class="error_tip" v-show="error_password2"&gt;两次输入的密码不一致&lt;/span&gt; &lt;/li&gt; &lt;li&gt; &lt;label&gt;手机号:&lt;/label&gt; &lt;input type="text" v-model="mobile" @blur="check_mobile" name="mobile" id="phone"&gt; &lt;span class="error_tip" v-show="error_mobile"&gt;[[ error_mobile_message ]]&lt;/span&gt; &lt;/li&gt; &lt;li&gt; &lt;label&gt;图形验证码:&lt;/label&gt; &lt;input type="text" name="image_code" id="pic_code" class="msg_input"&gt; &lt;img src="{{ static('images/pic_code.jpg') }}" alt="图形验证码" class="pic_code"&gt; &lt;span class="error_tip"&gt;请填写图形验证码&lt;/span&gt; &lt;/li&gt; &lt;li&gt; &lt;label&gt;短信验证码:&lt;/label&gt; &lt;input type="text" name="sms_code" id="msg_code" class="msg_input"&gt; &lt;a href="javascript:;" class="get_msg_code"&gt;获取短信验证码&lt;/a&gt; &lt;span class="error_tip"&gt;请填写短信验证码&lt;/span&gt; &lt;/li&gt; &lt;li class="agreement"&gt; &lt;input type="checkbox" name="allow" id="allow" v-model="allow" @change="check_allow"&gt; &lt;label&gt;同意”商城用户使用协议“&lt;/label&gt; &lt;span class="error_tip" v-show="error_allow"&gt;请勾选用户协议&lt;/span&gt; &lt;/li&gt; &lt;li class="reg_sub"&gt; &lt;input type="submit" value="注 册" @change="on_submit"&gt; {% if register_errmsg %} &lt;span class="error_tip2"&gt;{{ register_errmsg }}&lt;/span&gt; {% endif %} &lt;/li&gt; &lt;/ul&gt; 导入vue.js和ajax请求的js库准备register.js文件register.js文件主要处理注册页面的交互事件,并且向服务端提交注册表单请求下面是实现的前端校验逻辑以及表单提交逻辑 methods: { // 校验用户名 check_username() { let re = /^[a-zA-Z0-9_-]{5,20}$/; if (re.test(this.username)) { this.error_name = false; } else { this.error_name_message = '请输入5-20个字符的用户名'; this.error_name = true; // 校验密码 check_password() { let re = /^[0-9A-Za-z]{8,20}$/; this.error_password = !re.test(this.password); // 校验确认密码 check_password2() { if (this.password !== this.password2) { this.error_password2 = true; } else { this.error_password2 = false; // 校验手机号 check_mobile() { let re = /^1[3-9]\d{9}$/; if (re.test(this.mobile)) { this.error_mobile = false; } else { this.error_mobile_message = '您输入的手机号格式不正确'; this.error_mobile = true; // 校验是否勾选协议 check_allow() { this.error_allow = !this.allow; // 监听表单提交事件 on_submit() { this.check_username(); this.check_password(); this.check_password2(); this.check_mobile(); this.check_allow(); # 输入字段中有一个不符合规则就禁止 if (this.error_name === true || this.error_password === true || this.error_password2 === true || this.error_mobile === true || this.error_allow === true) { // 禁用表单的提交 window.event.returnValue = false; }后端业务注册逻辑在用户输完用户名之后,我们往往希望能够跟快的给出这个用户名是否符合注册需求,前面只是对用户名的规则进行了校验,还想知道他是否已经在系统注册过了,不然当用户都输完提交注册再给出用户名或者手机号已经注册过,体验不是特别好。所以需要在光标离开用户名输入框的时候就请求服务端来判断是否注册过。 path('register/', views.RegisterView.as_view(), name='register'), # name添加命名空间path('usernames/', views.UsernameCountView.as_view(), name="username"),re_path(r'mobiles/(?P1[3-9]d{9})', views.MobileCountView.as_view(), name='mobile')编写视图类 class UsernameCountView(View): def get(self, request, username): 查询该用户名是否在系统中存在 :param request: 请求对像 :param username: 前端传递的用户名 :return: count = User.objects.filter(username=username).count() return http.JsonResponse({'code':1001, 'msg':'用户已存在'}) if count == 1 \ else http.JsonResponse({'code': 1000, 'msg': ''}) 这里没有对响应做统一处理封装,后面专门介绍一下。 然后就是注册视图类的编写了: class RegisterView(View): """用户注册视图类""" def get(self, request): '''获取注册页面''' return render(request, 'register.html') def post(self, request): """""" username = request.POST.get('username') password = request.POST.get('password') password2 = request.POST.get('password2') mobile = request.POST.get('mobile') allow = request.POST.get('allow') # 判断参数是否齐全 if not all([username, password, password2, mobile, allow]): return http.HttpResponseForbidden('缺少必传参数') # 判断用户名是否是5-20个字符 if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', username): return http.HttpResponseForbidden('请输入5-20个字符的用户名') # 判断密码是否是8-20个数字 if not re.match(r'^[0-9A-Za-z]{8,20}$', password): return http.HttpResponseForbidden('请输入8-20位的密码') # 判断两次密码是否一致 if password != password2: return http.HttpResponseForbidden('两次输入的密码不一致') # 判断手机号是否合法 if not re.match(r'^1[3-9]\d{9}$', mobile): return http.HttpResponseForbidden('请输入正确的手机号码') # 判断是否勾选用户协议 if allow != 'on': return http.HttpResponseForbidden('请勾选用户协议') user = User.objects.create_user(username=username, password=password, mobile=mobile) except DatabaseError as e: return render(request, 'register.html', {'register_errmsg': e.args}) # 注册成功保存会话 login(request, user) return redirect(reverse('contents:index')) django提供的login方法,封装了写入session的操作,帮助我们快速登入一个用户,并实现状态保持,将通过认证的用户的唯一标识信息(比如:用户ID)写入到当前浏览器的 cookie 和服务端的 session 中。 request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)request.session[BACKEND_SESSION_KEY] = backendrequest.session[HASH_SESSION_KEY] = session_auth_hashsession会存入redis,之前在工程创建时进行session存储的配置 SESSION_ENGINE = "django.contrib.sessions.backends.cache"SESSION_CACHE_ALIAS = "session"原文地址https://www.cnblogs.com/zyjimmortalp/p/12464883.html

.Net Core 为 x86 和 x64 程序集编写 AnyCPU 包装

.Net Core 为 x86 和 x64 程序集编写 AnyCPU 包装前言这几天研究了一下 vJoy 这个虚拟游戏手柄驱动,感觉挺好玩的。但是使用时发现一个问题,C# SDK 中的程序集被分为 x86 和 x64 两个版本,如果直接在 AnyCPU 平台编译运行就有隐患,在32位系统中运行程序时会因为程序集版本不兼容而崩溃。这个 SDK 的两个版本文件名完全相同,根据 .Net 程序集的加载规则,我们是无法在不做任何工作的情况下实现共存的。对于平台特定程序集,目前的主流做法是把程序集放到以平台名称命名的文件夹中。通过一个包装程序集完成载入和调用。 正文我这里的包装使用了 .Net Core 的 AssemblyLoadContext 为中介完成 SDK 的动态加载。不过这种包装方式也有个很麻烦的地方,不能像正常引用的程序集那样享受各种智能提示,只能使用反射的方式调用实际库中的各种功能。为方便使用,包装库需要在内部完成反射处理并对外公开一套 API 方便使用。 定义程序集载入上下文因为资源由内部管理,不需要对外暴露,我这里的上下文是公开 API 的私有内部类,加载上下文的可重写方法中还有一个是用于非托管程序集的。这个上下文很智能,会自动查找被加载程序集的相同文件夹,所以无需重写,如果依赖程序集在其他位置,需要重写加载方法。 复制代码 1 private class VJoyAssemblyLoadContext : AssemblyLoadContext 2 { 3 public VJoyAssemblyLoadContext() : base(isCollectible: true) 4 { 5 } 6 7 protected override Assembly Load(AssemblyName name) 8 { 9 return null;10 }11 } 定义反射资源管理器其中 _is64BitRuntime 是关键,用 IntPtr.Size == 8 可以判断当前进程运行在 x86 还是 x64 模式,并在之后用于生成程序集加载路径。因为管理器的定位相当于驱动信息管理,所以只需要一个实例。在这里使用了私有构造方法加公共静态的实例获取方法来管理对象资源。 程序集载入后使用 Activator.CreateInstance 创建实例,然后使用 GetMethod 获取方法成员后就可以反射调用实例方法了。不过一直反射调用可能会造成巨大的性能损失,粗略估计最高可以达到上百倍,如果条件允许,最好能把方法缓存起来,以后调用既方便又高效。具体就是下面代码中定义的 Delegate 和 Fun&lt;&gt; 型变量,用于缓存方法信息。反射获取方法信息后可以调用 CreateDelegate 方法生成委托,静态方法和实例方法都可以。对于 Func&lt;&gt; 和 Action&lt;&gt; 来说需要进行强制类型转换。实际使用时尽量使用 Func&lt;&gt; 和 Action&lt;&gt;,如果方法的参数或返回值类型定义在动态加载的程序集中就只能使用 Delegate 来缓存了,同时也只能使用 DynamicInvoke 方法来调用。 下面的代码中的 DriverMatch 方法的参数有 ref 修饰符,对于这种参数类型,内置的 Func&lt;&gt; 无法封装,需要自行定义委托类型,我这里因为只用一次,所以就没管了,Invoke 调用的时候 .Net 会负责处理 ref、out、in 修饰符,相应地调用方法修改后的对象也会反映到传入的参数对象中。如果要定义相应的委托,看起来应该长这样: 复制代码public delegate TResult Func(in T1 arg1, out T2 arg2, ref T3 arg3);定义示例: 复制代码 1 public class VJoyControllerManager 2 { 3 private static readonly bool _is64BitRuntime = IntPtr.Size == 8; 4 private static readonly object _locker = new object(); 5 private static VJoyControllerManager _manager = null; 6 7 private VJoyAssemblyLoadContext _vJoyAssemblyLoadContext; 8 private Assembly _vJoyInterfaceWrapAssembly; 9 private object _joystick;10 private Type _vJoyType;11 //省略相似代码……12 13 private Delegate _getVJDStatusFunc;14 private Delegate _getVJDAxisExist;15 private Func _getVJDButtonNumber;16 //省略相似代码……17 18 public bool IsVJoyEnabled { get; }19 public bool DriverMatch { get; }20 public uint DllVer { get; }21 public uint DrvVer { get; }22 //省略相似代码……23 24 private VJoyControllerManager()25 {26 var path = Process.GetCurrentProcess().MainModule.FileName;27 var filePath = $@"{path.Substring(0, path.LastIndexOf('\'))}{(_is64BitRuntime ? "x64" : "x86")}vJoyInterfaceWrap.dll";28 29 _vJoyAssemblyLoadContext = new VJoyAssemblyLoadContext();30 _vJoyInterfaceWrapAssembly = _vJoyAssemblyLoadContext.LoadFromAssemblyPath(filePath);31 _joystick = Activator.CreateInstance(_vJoyInterfaceWrapAssembly.GetTypes().Single(t =&gt; t.Name == "vJoy"));32 _vJoyType = _joystick.GetType();33 34 IsVJoyEnabled = (bool)_vJoyType.GetMethod("vJoyEnabled").Invoke(_joystick, null);35 36 //省略相似代码……37 38 _getVJDButtonNumber = (Func)_vJoyType.GetMethod("GetVJDButtonNumber").CreateDelegate(typeof(Func), _joystick);39 40 var funcType = typeof(Func&lt;,&gt;).MakeGenericType(new Type[] { typeof(uint), _VjdStatEnumType });41 _getVJDStatusFunc = _vJoyType.GetMethod("GetVJDStatus").CreateDelegate(funcType, _joystick);42 43 funcType = typeof(Func&lt;,,&gt;).MakeGenericType(new Type[] { typeof(uint), _hidUsagesEnumType, typeof(bool) });44 _getVJDAxisExist = _vJoyType.GetMethod("GetVJDAxisExist").CreateDelegate(funcType, _joystick);45 46 var args = new object[] { 0u, 0u };47 DriverMatch = (bool)_vJoyType.GetMethod("DriverMatch").Invoke(_joystick, args);48 DllVer = (uint)args[0];49 DrvVer = (uint)args[1];50 }51 52 public static VJoyControllerManager GetManager()53 {54 if (_manager == null)55 lock (_locker)56 if (_manager == null)57 _manager = new VJoyControllerManager();58 59 return _manager;60 }61 62 public static void ReleaseManager()63 {64 if (_manager != null)65 lock (_locker)66 if (_manager != null)67 {68 _manager._axisEnumValues = null;69 //省略相似代码……70 71 _manager.UnLoadContext();72 _manager = null;73 }74 75 }76 77 private void UnLoadContext()78 {79 _vJoyAssemblyLoadContext.Unload();80 _vJoyAssemblyLoadContext = null;81 }82 83 public object GetVJDStatus(uint id) =&gt; IsVJoyEnabled ? _getVJDStatusFunc.DynamicInvoke(id) : null;84 85 public int GetVJDButtonNumber(uint id) =&gt; IsVJoyEnabled ? _getVJDButtonNumber(id) : 0;86 87 public bool GetVJDAxisExist(uint id, USAGES usages) =&gt; IsVJoyEnabled ? (bool)_getVJDAxisExist.DynamicInvoke(new object[] { id, _axisEnumValues[(int)usages] }) : false;88 89 //省略相似代码……90 } 剩下的部分基本就是用这个套路把剩下要用的东西封装缓存后供外界使用。感兴趣的话,完整代码可以在文章末尾到我的 GitHub 项目中查看。 结语用这个方法可以把分开的平台绑定程序集封装到一个 AnyCPU 程序集里。对于要与原生 dll 交互的项目,在原生封装时就完成这一步最好。如果封装也是分开的,这个办法就可以再封装一次,用来适配 AnyCPU 平台。 在我的 GitHub 项目中有个 vJoyDemo 项目,是使用示例。 转载请完整保留以下内容并在显眼位置标注,未经授权删除以下内容进行转载盗用的,保留追究法律责任的权利! 本文地址:https://www.cnblogs.com/coredx/p/12455761.html 完整源代码:Github 原文地址https://www.cnblogs.com/coredx/p/12455761.html

python框架Django实战商城项目之工程搭建

python框架Django实战商城项目之工程搭建项目说明该电商项目类似于京东商城,主要模块有验证、用户、第三方登录、首页广告、商品、购物车、订单、支付以及后台管理系统。项目开发模式采用前后端不分离的模式,为了提高搜索引擎排名,页面整体刷新采用jinja2模板引擎实现,局部刷新采用vue.js实现。 项目运行机制如下: 项目搭建工程创建项目使用码云进行源代码版本控制,在码云创建好后直接克隆到本地即可,然后在项目根目录下执行virtualenv venv创建虚拟环境,source venv/bin/activat激活虚拟环境后,安装django后,执行django-admin startproject immortal_mall创建django工程。 配置开发环境商城项目有两个环境,分别为测试环境和开发环境,django项目在创建完成后只有一个settings配置文件,但是两个环境需要两个配置文件,这里需要修改django获取配置文件的方式。新建settings包,再新建dev和pro两个配置文件,将默认生成的settings文件里面的内容拷贝至dev和pro文件里,结果如下 再在mange.py文件里指定开发环境需要的配置文件,生成环境的后面再说 配置jiaja2模板引擎安装jinja2扩展包pip install jinja2,然后在dev文件中配置一下 这里有个注意的点,如果在运行的时候报错了,提示 这时是因为注释掉了django默认的模板配置,需要去掉注释,只添加新的版本引起即可。 配置mysql数据库新建数据库新建数据库create database meiduo charset=utf8;新建mysql用户create user mall identified by '123456';授权用户只能访问immortal_mall数据库grant all on immortal_mall.* to 'mall'@'%';刷新授权flush privileges;配置数据库DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'immortal_mall', 'HOST': '127.0.0.1', 'USER': 'zhouyajun', 'PASSWORD': '12345678', 'PORT': '3306' }django默认使用的是mysqlclient工具,需要单独安装,这里用pymysql代替,在工程同名子目录的__init__.py文件中,写入下面代码 import pymysqlpymysql.install_as_MySQLdb()在启动项目对的时候可能会报错,提示mysql File "/Users/lixiang/.env/lib/python3.6/site-packages/django/db/backends/mysql/base.py", line 36, in raise ImproperlyConfigured('mysqlclient 1.3.13 or newer is required; you have %s.' % Database.__version__) django.core.exceptions.ImproperlyConfigured: mysqlclient 1.3.13 or newer is required; you have 0.9.3.具体解决方式可以参考这里https://zhuanlan.zhihu.com/p/76920424 配置redis商城采用redis作为缓存服务pip install django-redis这里是django-redis的使用文档https://django-redis-chs.readthedocs.io/zh_CN/latest/,在dev文件中配置redis 缓存配置CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/2", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "session": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/3", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }SESSION_ENGINE = "django.contrib.sessions.backends.cache"SESSION_CACHE_ALIAS = "session"default是redis采用的默认配置,用的是2号数据库,session是redis保持保持状态的配置项,用的是3号数据库, LOGGING = { 'version': 1, 'disable_existing_loggers': False, # 是否禁用已经存在的日志器 'formatters': { # 日志信息显示的格式 'verbose': { 'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s' 'simple': { 'format': '%(levelname)s %(module)s %(lineno)d %(message)s' 'filters': { # 对日志进行过滤 'require_debug_true': { # django在debug模式下才输出日志 '()': 'django.utils.log.RequireDebugTrue', 'handlers': { # 日志处理方法 'console': { # 向终端中输出日志 'level': 'INFO', 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', 'formatter': 'simple' 'file': { # 向文件中输出日志 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(os.path.dirname(BASE_DIR), 'logs/mall.log'), # 日志文件的位置 'maxBytes': 300 * 1024 * 1024, 'backupCount': 10, 'formatter': 'verbose' 'loggers': { # 日志器 'django': { # 定义了一个名为django的日志器 'handlers': ['console', 'file'], # 可以同时向终端与文件中输出日志 'propagate': True, # 是否继续传递日志信息 'level': 'INFO', # 日志器接收的最低日志级别 }这里需要在项目的根目录下手动创建logs文件,写日志的时候呢,我们希望能够自动分日期去写,每天的日志写到不同的文件里,这里可以使用python模块自带的TimedRotatingFileHandler,也可以自定义一个handler类去实现。我这里呢自己实现了一个类叫做MallRotatingFileHandler效果如下: 该类实现按月划分日志文件,自动按照当天日期命名日志文件,也可以定义文件容量。 配置前端静态文件准备静态文件夹static 指定静态文件加载路径 指定加载静态恩建路由前缀 STATIC_URL = '/static/' 配置静态文件加载路径 STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]运行项目,请求获取一张图片http://127.0.0.1:8989/static/images/adv01.jpg,成功获取表示配置正确。 最终项目各个目录展示成果: 原文地址https://www.cnblogs.com/zyjimmortalp/p/12448687.html

一个轻量级的基于 .NET Core 的 ORM 框架 HSQL

一个轻量级的基于 .NET Core 的 ORM 框架 HSQLHSQL 是一种轻量级的基于 .NET Core 的数据库对象关系映射「ORM」框架 HSQL 是一种可以使用非常简单且高效的方式进行数据库操作的一种框架,通过简单的语法,使数据库操作不再成为难事。目前支持的数据库有 MySql、SQLServer。  安装方法Install-Package HSQL-standard使用方法创建映射模型创建数据库操作实例进行数据库操作新增批量新增修改删除查询单实例查询分页查询灵活条件查询性能无索引、单机、单表、表数据为十万行 单实例插入十万次批量插入十万次查询单实例十万次 创建映射模型 [Table("t_student")]public class Student{ [Column("id")] public string Id { get; set; } [Column("name")] public string Name { get; set; } [Column("age")] public int Age { get; set; } [Column("school_id")] public string SchoolId { get; set; } [Column("birthday")] public long Birthday { get; set; } Table 标记一个表对象。如:[Table("t_student")] 代表 Student 类将映射为数据库表 t_studentColumn 标记一个列对象。如:[Column("id")] 代表 Id 属性将映射为数据库列 id 创建数据库操作实例var connectionString = $"Server=127.0.0.1;Database=test;Uid=root;Pwd=123456;";var database = new Database(Dialect.MySQL, connectionString);connectionString 为数据库连接字符串。Dialect.MySQL 表示访问数据库的类型为 MYSQL var result = database.Insert(new Student(){ Name = "zhangsan", Age = 18, SchoolId = "123" Insert 方法可插入一个对象,表示对 t_student 表插入一条数据。最后被解释为 SQL 语句 -&gt;INSERT INTO t_student(id,name,age,school_id,birthday) VALUES(@id,@name,@age,@school_id,@birthday); var list = new List();for (var i = 0; i &lt; 1000; i++){ list.Add(new Student() Id = $"{i}", Name = "zhangsan", Age = 18, SchoolId = "123" }var result = database.Insert(list); Insert 方法可插入一个集合对象,表示对 t_student 表进行批量插入。最后被解释为事务性批量插入的 SQL 语句,如INSERT INTO t_student(id,name,age,school_id,birthday) VALUES(@id,@name,@age,@school_id,@birthday);会进行多条语句事务操作。 修改var result = database.Update(x =&gt; x.Id.Contains("test_update_list"), new Student() { Age = 19 });Update 方法表示更新操作。如:参数1:x =&gt; x.Id.Contains("test_update_list") 被解释为 WHERE id LIKE '%test_update_list%'参数2:new Student() { Age = 19 } 被解释为 SET age = @age最终SQL语句为:UPDATE t_student SET age = @age WHERE id LIKE '%test_update_list%'; 删除var result = database.Delete(x =&gt; x.Age &gt; 0);Delete 方法表示删除操作。最终被解释为 SQL 语句:DELETE FROM t_student WHERE age &gt; 0; 查询var list = database.Query(x =&gt; x.Age == 19 &amp;&amp; x.Id.Contains("test_query_list")).ToList();Query =&gt; ToList 方法表示查询操作。最终被解释为 SQL 语句:SELECT id,name,age,school_id,birthday FROM t_student WHERE age = 19 AND id LIKE '%test_query_list%'; 单实例查询var student = database.Query(x =&gt; x.Age == 19 &amp;&amp; x.Id.Equals("test_query_single")).FirstOrDefault();Query =&gt; ToList 方法表示查询操作:当 Dialect 为 MySQL 时 最终被解释为 SQL 语句:SELECT id,name,age,school_id,birthday FROM t_student WHERE age = 19 AND id = 'test_query_single' LIMIT 0,1;当 Dialect 为 SQLServer 时 最终被解释为 SQL 语句:SELECT TOP 1 id,name,age,school_id,birthday FROM t_student WHERE age = 19 AND id = 'test_query_single'; 分页查询var list = database.Query(x =&gt; x.Age == 19 &amp;&amp; x.Id.Contains("test_query_page_list")).ToList(2, 10);Query =&gt; ToList(2,10) 方法表示分页查询操作,pageIndex 为第几页,pageSize 为每页记录条数。最终被解释为 SQL 语句:SELECT id,name,age,school_id,birthday FROM t_student WHERE age = 19 AND id LIKE '%test_query_page_list%' LIMIT 10,10; 灵活条件查询var list = database.Query(x =&gt; x.Age == 19 &amp;&amp; x.Id.Contains("test_query_page_list")).AddCondition(x =&gt; x.Name == "zhangsan").ToList(2, 10);AddCondition 方法可以对查询进行动态增加条件。最终解释的 SQL 的 WHERE 部分会包含 AND name = 'zhangsan' 单实例插入十万次 var database = new Database(Dialect.MySQL, connnectionString);database.Delete(x =&gt; x.Age &gt;= 0);var list = new List();for (var i = 0; i &lt; 100000; i++){ list.Add(new Student() Id = $"{i}", Name = "zhangsan", Age = 18, SchoolId = "123" var stopwatch = new Stopwatch();stopwatch.Start();list.ForEach(x =&gt;{ var result = database.Insert&lt;Student&gt;(x); });stopwatch.Stop();var elapsedMilliseconds = $"插入十万条次共耗时:{stopwatch.ElapsedMilliseconds}毫秒"; 第一次测试 -&gt; 插入十万条次共耗时: 111038 毫秒,平均单次插入耗时: 1.11038 毫秒第二次测试 -&gt; 插入十万条次共耗时: 109037 毫秒,平均单次插入耗时: 1.09037 毫秒 批量插入十万次 var database = new Database(Dialect.MySQL, connnectionString);database.Delete(x =&gt; x.Age &gt;= 0);var list = new List();for (var i = 0; i &lt; 100000; i++){ list.Add(new Student() Id = $"{i}", Name = "zhangsan", Age = 18, SchoolId = "123" var stopwatch = new Stopwatch();stopwatch.Start();var result = database.Insert(list);stopwatch.Stop();var elapsedMilliseconds = $"插入十万次共耗时:{stopwatch.ElapsedMilliseconds}毫秒"; 第一次测试 -&gt; 插入十万次共耗时: 11177 毫秒,平均单次查询耗时: 0.11177 毫秒第二次测试 -&gt; 插入十万条次共耗时: 10776 毫秒,平均单次查询耗时: 0.10776 毫秒 查询单实例十万次 var database = new Database(Dialect.MySQL, connnectionString);database.Delete(x =&gt; x.Age &gt;= 0);var list = new List();for (var i = 0; i &lt; 100000; i++){ list.Add(new Student() Id = $"{i}", Name = "zhangsan", Age = 18, SchoolId = "123" var stopwatch = new Stopwatch();stopwatch.Start();for (var i = 0; i &lt; 100000; i++){ var student = database.Query&lt;Student&gt;(x =&gt; x.Age == 18 &amp;&amp; x.Id.Equals($"{i}") &amp;&amp; x.SchoolId.Equals("123")).FirstOrDefault(); }stopwatch.Stop();var elapsedMilliseconds = $"查询十万次共耗时:{stopwatch.ElapsedMilliseconds}毫秒"; 十万条数据时:第一次测试 -&gt; 查询十万条次共耗时: 877936‬ 毫秒,平均单次查询耗时: 8.77936 毫秒第二次测试 -&gt; 查询十万条次共耗时: 874122‬ 毫秒,平均单次查询耗时: 8.74122 毫秒  项目地址:https://github.com/hexu6788 如果你觉得本篇文章对您有帮助的话,感谢您的【推荐】。 如果你对 .NET 有兴趣的话可以关注我,我会定期的在博客分享我的学习心得。 本文地址:http://www.cnblogs.com/hexu6788/p/12435814.html 作者博客:何旭

Spring Boot:如何优雅的使用 Mybatis

Spring Boot(四):如何优雅的使用 Mybatis一、前言 Orm框架的本质是简化编程中操作数据库的编码,发展到现在,基本上就剩宣称不用谢一句sql的hibernate,一个是可以灵活调试动态sql的mybatis,两者各有特点,在企业级系统来发中可以根据需求灵活使用。发现一个有趣的现象:传统企业大都喜欢hibernate,互联网行业通常使用mybatis。 hibernate特点就是所有的sql都用java代码来生成,不用跳出程序去写sql,有这编程的完整性,发展到最顶端就是spring data jpa这种模式,基本上根据方法名就可以生成对应的sql。 mybatis初期使用比较麻烦,需要各种配置文件、实体类、Dao层映射关系、还有一大堆其他配置文件。 当然mybatis也有发现了这种弊端,初期开发了generator可以根据表结构自动生成实体类、配置文件和dao层代码,可以减轻一部分开发量;后期也进行了大量的优化可以使用注解,自动管理dao层和配置文件等,发展到最顶级就是今天讲的这种springboot+mybatis可以完全注解不用配置文件,也可以简单配置轻松上手。 springboot就是牛逼呀,啥玩意关联springboot都能化繁为简。 二、mybatis-spring-boot-starter mybatis-spring-boot-starter主要由两种解决方案,一种是使用注解解决一切问题,一种的简化后的老传统。 当然任何模式都需要先引入mybatis-spring-boot-starter的pom文件,现在最新版本是 &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt; &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;2.1.1&lt;/version&gt; 三、无配置注解版 1、添加maven文件 &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-data-jdbc&lt;/artifactId&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt; &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;2.1.1&lt;/version&gt; &lt;groupId&gt;com.oracle.ojdbc&lt;/groupId&gt; &lt;artifactId&gt;ojdbc8&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; https://mvnrepository.com/artifact/com.alibaba/druid --> &lt;groupId&gt;com.alibaba&lt;/groupId&gt; &lt;artifactId&gt;druid&lt;/artifactId&gt; &lt;version&gt;1.1.21&lt;/version&gt; https://mvnrepository.com/artifact/log4j/log4j --> &lt;groupId&gt;log4j&lt;/groupId&gt; &lt;artifactId&gt;log4j&lt;/artifactId&gt; &lt;version&gt;1.2.17&lt;/version&gt; 2、application.yml添加相关配置 spring: datasource: username: mine password: mine url: jdbc:oracle:thin:@127.0.0.1:1521:orcl driver-class-name: oracle.jdbc.driver.OracleDriver type: com.alibaba.druid.pool.DruidDataSource initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 Spring Boot 会自动加载spring.datasource.*相关配置,数据源就会自动注入到 sqlSessionFactory 中,sqlSessionFactory 会自动注入到 Mapper 中,对了,你一切都不用管了,直接拿起来使用就行了。 在启动类中添加对 mapper 包扫描@MapperScan @MapperScan(value="com.demo.mapper")@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}或者直接在 Mapper 类上面添加注解@Mapper,建议使用上面那种,不然每个 mapper 加个注解也挺麻烦的 3、controller @RestControllerpublic class DeptController { @Autowired DepartmentMapper departmentMapper; @GetMapping("/dept/{id}") public Department getDepartment(@PathVariable("id") Integer id){ return departmentMapper.getDeptById(id); @GetMapping("/dept") public int insertDeptById(@PathVariable("id") Integer id,@PathVariable("departmentName") String departmentName){ int ret = departmentMapper.insertDept(id,departmentName); return ret; }4、开发mapper package com.demo.mapper; import com.demo.bean.Department;import org.apache.ibatis.annotations.*; public interface DepartmentMapper { @Select("select * from SSH_DEPARTMENT where id=#{id}") public Department getDeptById(Integer id); @Delete("delete from SSH_DEPARTMENT where id=#{id}") public int deleteDeptById(Integer id); @Options(useGeneratedKeys = true,keyProperty = "id") @Insert("insert into SSH_DEPARTMENT(department_name) values(#{departmentName})") public int insertDept(Department department); @Update("update SSH_DEPARTMENT set departmentName=#{DEPARTMENT_NAME} where id=#{id}") public int updateDept(Department department); }@Select 是查询类的注解,所有的查询均使用这个@Result 修饰返回的结果集,关联实体类属性和数据库字段一一对应,如果实体类属性和数据库属性名保持一致,就不需要这个属性来修饰。@Insert 插入数据库使用,直接传入实体类会自动解析属性到对应的值@Update 负责修改,也可以直接传入对象@delete 负责删除4、运行 上面三步就基本完成了相关 Mapper 层开发,使用的时候当作普通的类注入进入就可以了 (1)查询 (2)插入 插入前数据库状态 浏览器调用controller执行插入 插入后结果查询 四、极简XML版本 极简 xml 版本保持映射文件的老传统,接口层只需要定义空方法,系统会自动根据方法名在映射文件中找对应的 Sql pom 文件和上个版本一样,只是application.yml新增以下配置 mybatis.config-location=classpath:mybatis/mybatis-config.xmlmybatis.mapper-locations=classpath:mybatis/mapper/*.xmlmybatis-config.xml 配置 &lt;?xml version="1.0" encoding="UTF-8" ?&gt;/p&gt; PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"&gt; &lt;settings&gt; &lt;setting name="mapUnderscoreToCamelCase" value="true"/&gt; &lt;/settings&gt; 2、employeeMapper.xml &lt;?xml version="1.0" encoding="UTF-8" ?&gt;/p&gt; PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"&gt;

Linux文件同步工具之rsync

学习背景1.最近公司的项目在使用jenkins做自动化构建,因为jenkins在构建时是比较耗性能的,便单独使用了一台服务器做构建服务器。但是个人觉得这样成本过高,单独拿一台服务器来构建并且该服务器配置不能太差。便想着通过在本地搭建一台jenkins服务,做构建使用。但是这样有一个问题,无法使用webhooks,便选择的轮训构建。其实可以使用内网穿透的方式解决的,该实例还在学习中,也实验成功了,由于使用还不是特别熟悉,后期单独写一篇文章分享。 2.在jenkins构架时,我们需要配置一个work目录,如/root/jenkins/workspace/daodao_system这样的目录,但是我们需要使用nginx配置项目目录,肯定是不可能配置到这个jenkins的工作目录的。通过了解,Linux的同步命令rsync命令可以实现文件同步。这里指的文件同步就是将jenkins工作目录下面的文件同步到我们指定的目录(如我们nginx配置的项目目录)。并且rsync同步命令具备,高效、带宽消耗低、支持复制链接、设备、属主、属组、权限等。 命令介绍rsync是远程(或本地)复制和同步文件最常用的命令。 借助rsync命令,你可以跨目录,跨磁盘和跨网络远程与本地数据进行复制和同步。举例来说:在两台Linux主机之间进行数据备份和镜像。本文介绍在Linux主机上进行远程和本地传输文件的常见用法,不需要root账户也可以允许rsync。 rsync命令特点1.高效地复制同步数据到对端,或者对端到本地。 2.支持复制链接、设备、属主、属组、权限。 3.比scp(Secure Copy)更快。rsync使用远程更新协议( remote-update protocol ),这允许仅仅传输两组文件之间的差异。对于首次传输,它将文件或目录的全部内容从源复制到目标,但是从下次起,它仅将变化部分复制到目标。 4.rsync消耗较少的带宽,因为它使用压缩和解压缩方法,同时发送和接收数据两端。HTTP压缩技术。 命令的安装本演示文章的环境是centos7.0的版本,可以直接使用。如需安装可以参考该文章 命令语法格式rsync [options] 资源源目录 目标目录-v : 详细模式输出 -r : 递归拷贝数据,但是传输数据时不保留时间戳和权限 -a : 归档模式, 归档模式总是递归拷贝,而且保留符号链接、权限、属主、属组时间戳 -z : 压缩传输 -h : human-readable --progress: 显示传输过程 --exclude=PATTERN 指定排除传输的文件模式 --include=PATTERN 指定需要传输的文件模式 --delete 同步时,删除那些DST中有,而SRC没有的文件 --max-size:限定传输文件大小的上限 --dry-run:显示那些文件将被传输,并不会实际传输 --bwlimit:限制传输带宽 -W:拷贝文件,不进行增量检测 使用案例本地同步某个文件 [root@iZ7eetumtw8c9jZ jenkins]# rsync -zvrh remoting.jar /root/work/sending incremental file listcreated directory /root/workremoting.jar sent 796.37K bytes received 68 bytes 1.59M bytes/sectotal size is 872.44K speedup is 1.10同步结果查看 [root@iZ7eetumtw8c9jZ jenkins]# ll /root/work/total 852-rw-r--r-- 1 root root 872440 Nov 3 20:03 remoting.jar同步某个目录 [root@iZ7eetumtw8c9jZ jenkins]# rsync -zvrh workspace/ /root/work/sending incremental file list 公司服务器测试/tests/Unit/公司服务器测试/tests/Unit/ExampleTest.php公司服务器测试@tmp/ sent 18.82M bytes received 7.30K bytes 7.53M bytes/sectotal size is 29.87M speedup is 1.59同步结果查看 [root@iZ7eetumtw8c9jZ jenkins]# ll /root/work/total 868-rw-r--r-- 1 root root 872440 Nov 3 20:03 remoting.jardrwxr-xr-x 11 root root 4096 Nov 3 20:05 公司服务器测试drwxr-xr-x 2 root root 4096 Nov 3 20:04 公司服务器测试@tmp 同步远程目录 [root@tecmint]$ rsync -avz rpmpkgs/ root@192.168.0.101:/home/root@192.168.0.101's password:sending incremental file list./httpd-2.2.3-82.el5.centos.i386.rpmmod_ssl-2.2.3-82.el5.centos.i386.rpmnagios-3.5.0.tar.gznagios-plugins-1.4.16.tar.gzsent 4993369 bytes received 91 bytes 399476.80 bytes/sectotal size is 4991313 speedup is 1.00总结使用该命令,最吸引我的地方就是可以实现增量同步,而不是全量同步。这样减少带宽的消耗,也加快了传输的速度。原文地址https://www.cnblogs.com/qqblog/p/11788969.html

【Linux】定时任务-crontab

【Linux】定时任务-crontab1.1 定时任务crond介绍Crond是linux系统中用来定期执行命令/脚本或指定程序任务的一种服务或软件,一般情况下,安装完centos 6/7等linux操作系统之后,默认便会启动crond任务调度服务,crond服务也会定期(默认每分钟检查一次)检查系统中是否有要执行的任务工作,如果有,便会根据其预先设定的定时任务规则自动执行该定时任务工作,这个crond定时任务服务就相当于“闹钟”一样。 1.2 Linux系统的定时任务Linux系统中定时任务调度的工作可分为以下两种情况: 一、 linux系统自身定期执行的任务工作:系统周期性自行执行的任务工作,如轮询系统日志,备份系统数据,清理系统缓存等,这些任务无需我们人为干预。 [root@node1 ~]# ls -l /var/log/messages* # 系统的日志-rw-------. 1 root root 36080506 9月 1 11:56 /var/log/messages-rw-------. 1 root root 26616060 8月 11 15:44 /var/log/messages-20190811-rw-------. 1 root root 26112559 8月 19 10:23 /var/log/messages-20190819[root@node1 ~]# ls -l /var/log/secure* # 用户登录日志-rw-------. 1 root root 10021 9月 1 11:56 /var/log/secure-rw-------. 1 root root 15821 8月 11 14:44 /var/log/secure-20190811-rw-------. 1 root root 9131 8月 19 09:51 /var/log/secure-20190819二、用户执行的任务工作:某个用户或系统管理员定期要做的任务工作,例如每天晚上0点对tomcat日志进行切割及备份数据库数据,一般这些工作都需要由运维自行设置才行。 实例:每晚0点对tomcat日志进行切割 [root@node1 ~]# crontab –l0 0 * /bin/sh /server/scripts/tomcat_backup.sh &gt;/dev/null 2&gt;&amp;1用户执行的工作,也就是我们运维管理人员执行的任务工作,因此用户执行的任务是本篇文章的重点。 1.2.2 Linux系统下的定时任务软件的种类严格说,linux系统下的定时任务还真不少,例如:at,crontab,anacron 假如:某天晚上需要处理一个任务,仅仅是这一天的晚上,属于突发性的工作任务。要执行at命令,还需要启动一个atd的服务才行,在实际工作中,需要用到的时候几乎没有,因此此处也不详解。 crontab命令正如前面所说的这个命令可以周期性的执行任务工作。 例如:每晚0点备份数据库数据。如果要执行crontab这个命令,也需要启动一个服务crond才行,此命令也是生产环境最常使用到的命令。 Centos7系统 查看crond服务是否开机自启 [root@node1 ~]# systemctl list-unit-files |grep crondcrond.service enabled 注:centos6系统应使用如下命令: chkconfig –-list |grep crond查看crond服务进程 [root@node1 ~]# ps -elf|grep [c]rond4 S root 633 1 0 80 0 - 31555 hrtime 14:03 ? 00:00:00 /usr/sbin/crond -nanacron:此命令主要用于非7*24小时开机的服务器准备的,anacron并不能指定具体时间执行任务工作,而是以天为周期或者在系统每次开机后执行的任务工作。它会检测服务器停机期间应该执行,但是并没有执行的任务工作,并将该任务执行一遍。 crond服务是运行的程序,而crontab是用来管理用户的定时任务(规则)的命令crond服务是企业生产工作中常用的重要服务,at和anacron可忽略使用方法几乎每个服务器都会用到crond服务1.3 定时任务crond使用说明1.3.1 指令语法crontab 【-u user】 {-l|-e|-r|-i} 查看系统帮助 man crontab[root@node1 ~]# crontab --helpcrontab:无效选项 -- -crontab: usage error: unrecognized optionUsage: crontab [options] file crontab [options] crontab -n [hostname] Options: -u define user -e edit user's crontab -l list user's crontab -r delete user's crontab -i prompt before deleting -n set host in cluster to run users' crontabs -c get host in cluster to run users' crontabs -s selinux context -x enable debugging Default operation is replace, per 1003.2crontab –l查看当前用户的定时任务配置 [root@node1 ~]# crontab –l0 0 * /bin/bash /server/scripts/tomcat_log_cut.sh &gt;/dev/null 2&gt;&amp;1 #tomcat日志切割crontab –e进入当前用户的定时任务vim编辑模式 [root@node1 ~]# crontab –e tomcat日志切割 0 0 * /bin/bash /server/scripts/tomcat_log_cut.sh &gt;/dev/null 2&gt;&amp;1~ ~crontab –u 用户名 –l查看指定用户的定时任务设置 [root@node1 ~]# crontab -u root –l tomcat日志切割 0 0 * /bin/bash /server/scripts/tomcat_log_cut.sh &gt;/dev/null 2&gt;&amp;1 [root@node1 ~]# crontab -u node1 -lno crontab for node11.3.2 指令说明通过crontab我们可以在固定的间隔时间执行指定的系统指令或script脚本。时间间隔的单位可以是分钟,小时,日,月,周及以上的任意组合。(注意:日和周不要组合) crond服务通过crontab命令可以很容易的实现周期性的日志分析或数据备份等企业运维场景工作 /5 * /bin/bash /server/scripts/tomcat_log_cut.sh &gt;/dev/null 2&gt;&amp;1说明: 五个星含义::分钟 /5代表每隔5分钟:小时:日:月:星期/bin/sh /server/scripts/tomcat_log_cut.sh :需要执行的命令或脚本(命令必须全路径,可使用“which 命令”,查看命令路径) 1.3.3 使用者权限及定时任务文件文件 说明/etc/cron.deny(拒绝) 该文件中所列用户不允许使用crontab/etc/cron.allow(允许) 该文件优先级高于cron.deny(默认不存在,一般不用)/var/spool/cron/ 所有用户crontab配置文件默认都存在此目录,文件名以用户名命名[root@node1 ~]# cat /etc/cron.deny node1[root@node1 ~]# su - node1[node1@node1 ~]$ crontab -lYou (node1) are not allowed to use this program (crontab)See crontab(1) for more information[node1@node1 ~]$ crontab -eYou (node1) are not allowed to use this program (crontab)See crontab(1) for more information [root@node1 ~]#[root@node1 ~]# ls /var/spool/cron/root #默认是没有的,只有此用户创建了定时任务才有[root@node1 ~]# cat /var/spool/cron/root 0 0 * /bin/sh /server/scripts/tomcat_log_cut.sh &gt;/dev/null 2&gt;&amp;1 #tomcat日志切割1.3.4 指令选项说明含义表参数 含义-l(字母) 查看crontab文件内容-e 编辑crontab文件内容-r 删除crontab文件内容-u user 指定使用的用户执行任务==特别强调:-r参数在生产中很少用,也请各位慎用,使用时,请大家务必怀着对rm –rf 一样的态度,能够不用,就绝对不用!!!== 补充:crontab{-l | -e }实际上就是在操作/var/spool/cron/当前用户这样的文件 使用crontab命令的优点: crontab 可以检查语法输入方便1.3.5 定时任务指令的使用格式默认情况下,当用户建立定时任务规则后,该规则记录对应的配置文件会存在于/var/spool/cron中,其crontab配置文件对应的文件名与登录的用户名一致,如:root用户的定时任务配置文件为/var/spool/cron/root crontab定时任务的书写格式很简单,用户的定时任务规则一般分为6个段(每个段通过空格来分隔,系统的定时任务为/etc/crontab,分为七个段,以空格来分割),前五段为时间的设定段,第六段为所要执行的命令或脚本任务段 [root@node1 ~]# cat /etc/crontab Example of job definition: .---------------- minute (0 - 59) 分钟 0-59 | .------------- hour (0 - 23) 小时 | | .---------- day of month (1 - 31) 日期 | | | .------- month (1 - 12) OR jan,feb,mar,apr ... 月份 | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR 星期 * user-name command to be executed 分 时 日 月 周 1.3.6 crontab语法格式中时间段的含义如下表段 含义第一段 代表分钟第二段 代表小时第三段 代表日,天第四段 代表月份第五段 代表星期,周几==提示:时间记忆口诀:分时日月周。 取值范围口诀,正常日期时间范围。== 1.3.7 crontab语法格式中特殊符号含义如下表特殊符号 含义 *号,表示任意时间都,实际就是“每”的意思 减号表示分隔符,表示一个时间范围,区间段,如17点到19点 例如:每天的17,18,19点的00分执行任务。00 17-19 * cmd , 逗号,表示分隔时段的意思 例如:每天的5点和10点00分执行任务,00 5,10 * cmd/n N代表数字,即“每隔n单位时间”。 例如:每10分钟执行一次任务可以写成 /10 * cmd ,其中,/10的范围是0-59,因此也可以写成0-59/101.3.8 图片说明crontab使用方法image 1.3.9 用户定时任务实例* command 每1分钟执行一次command3,15 * command 每小时的第3和第15分钟执行3,15 8-11 * command 在上午8点到11点的第3和第15分钟执行3,15 8-11 /2 * command 每隔两天的上午8点到11点的第3和第15分钟执行3,15 8-11 1 command 每个星期一的上午8点到11点的第3和第15分钟执行22 14 0 command 每周日的14点22分执行02 04 * command 每天4点的02分钟执行1.4 附带说明1.4.1 crontab书写格式说明/dev/null 2&gt;&amp;1实例 0 0 * /bin/sh /shell/tomcat_log.sh&gt;/dev/null 2 &gt;&amp;1 解释 0是标准输入 使用&lt;或&lt;&lt;1是标准输出 使用&gt;或&gt;&gt;2是标准错误输出 使用2&gt;或2&gt;&gt;/dev/null 2&gt;&amp;1 即错误输出与标准输出全部重定向到空,可以写成1&gt;/dev/null 2&gt;/dev/null 关于重定向的作用 重定向到空可以避免碎片文件占用inode资源重定向到一个指定log里,可以看任务是否执行1.4.2 编写定时任务注意点每个任务添加注释,谁写的,什么时间写的,完成什么需求执行脚本使用/bin/sh(防止脚本无执行权限),要执行的文件路径是从根开始的绝对路径(防止找不到文件)尽量把要执行的命令放在脚本里,然后把脚本放在定时任务里。对于调用脚本的定时任务,可以把标准输出错误输出重定向到空定时任务中带%无法执行,需要加转义如果时上有值,分钟上必须有值日和周不要同时使用,会冲突 与&gt;/dev/null 2&gt;&amp;1不要同时存在 1.4.3 常见故障及解决方法一、日期错误 crontab编辑后报错 crontab: installing new crontab "/tmp/crontab.QZzQuN":1: bad minute报错原因:crontab –e只写了命令,日期出现错误或者未填写 解决方法:crontab –e重新编辑定时任务,正确书写格式 二、inode号耗尽 设置crontab时提示:No space left on device 定位故障: 使用df –k检查还有空间使用df –i显示/var已占用100%,如果inode耗尽,则系统上将不能创建文件在/var/spoo/clientmqueue/有超多的文件ls半天没反应使用rm –rf * 会自动跳出root,可使用xargs来解决cd /var/spool/clientmqueue ls | xargs rm –f故障分析:系统中cron执行的程序有输出内容,输出内容会以邮件形式发给cron的用户,而sendmail没有启动就产生了这些文件 解决方法:将crontab里面的命令后面加上&gt;/dev/null 2&gt;&amp;1原文地址https://www.cnblogs.com/BabySermonizer/p/11444154.html

Java异常处理机制

Java异常处理机制一. 异常类型 Exception  Exception主要分为两种:Runtime Exception、Checked(Compile) Exception。   常见的Runtime Exception,有:NullPointerException、ArithmeticException...   常见的Checked(Compile) Exception,有:IOException、FileNotFoundException...   所谓Checked Exception就是在编译期间,你必须对某条、或多条语句进行异常处理,如: try...catch...、throws语句。   下面介绍下Checked Exception的优缺点: 特点与优点: Java专有,体现Java的设计哲学,没有完善错误处理的代码根本就不会给你机会去执行。缺点:必须显式捕捉并处理异常,或显式声明抛出异常,增加程序复杂度。若显式抛出异常,则会增加方法签名与异常的耦合度。 Error  Error主要表示一些虚拟机内部错误,如:动态链接失败。 二. 异常处理规则程序可读性:避免过度使用异常处理代码,减少方法签名与异常的耦合度。异常原始性:捕获并保留原始异常信息。异常针对性:根据业务需求决定如何处理异常,比如:当你检查商品库存时发生异常,此时就应终止此次调用,并告诉上层用户详细、明确的原因。当你获取用户头像失败时,因为该操作不影响整体订单、支付流程,所以不需要终止此次调用,可与上层用户协商处理,比如:返回一个空字符串。三. 相关问题throw与throws区别?位置:throws位于方法签名。throw位于函数体内。语法格式throws后面跟的是异常类,且一次可以跟多个,只需要以逗号分隔。throw后面跟着的是异常实例,且一次只能跟一个。命中率throws只是做个防守,并不会真正执行。一旦执行到throw语句,必定抛出异常。 为什么要有异常处理机制?无法穷举所有的异常情况。若异常处理的代码过多,会导致程序可读性变差。 为什么要把原始异常封装一层?安全性,防止恶意用户获得系统内部信息。对上层用户更加友好,让其更加明确、详细的知道异常原因。 为什么有那么多类需要实现Closeable或AutoCloseable接口?Java9增强了自动关闭资源的try语句。复制代码public class ExceptionTest { public static void readFile(){ try(BufferedReader bufferedReader = new BufferedReader(new FileReader("justForTest.txt"))) System.out.println(bufferedReader.readLine()); } catch (IOException e) { e.printStackTrace(); @Testpublic void readFileTest(){ ExceptionTest.readFile(); // Just for test. }复制代码 四. 实践(自定义RuntimeException) 复制代码@Getterpublic class ServiceException extends RuntimeException { private HttpStatus status; private ResultCode resultCode; private Object errorData; private ServiceException(HttpStatus status, ResultCode resultCode, Object errorData){ this.status = status; this.resultCode = resultCode; this.errorData = errorData; public static ServiceException badRequest(ResultCode resultCode, Object errorData){ return new ServiceException(HttpStatus.BAD_REQUEST, resultCode, errorData); @Getterpublic enum ResultCode { // errorCode SUCCESS(0, "SUCCESS"), INVALID_PARAMETER(600, "invalid parameter"); private final int errorCode; private final String errorData; ResultCode(int errorCode, String errorData){ this.errorCode = errorCode; this.errorData = errorData; @ControllerAdvicepublic class GlobalErrorHandler { @ExceptionHandler(ServiceException.class) public ResponseEntity&lt;ErrorResponse&gt; handleServiceException(ServiceException ex){ return ResponseEntity .status(ex.getStatus()) .body(ErrorResponse.failed(ex.getResultCode(), ex.getErrorData())); @ApiModel@Getterpublic class ErrorResponse implements Serializable { private static final long serialVersionUID = -2254339918462802230L; private final int errorCode; private final String errorMsg; private final T errorData; private ErrorResponse(ResultCode resultCode, T errorData) { this.errorCode = resultCode.getErrorCode(); this.errorMsg = resultCode.getErrorData(); this.errorData = errorData; public static &lt;T&gt; ErrorResponse&lt;T&gt; failed(ResultCode resultCode, T data){ return new ErrorResponse(resultCode, data); @RestControllerpublic class OrderController { @GetMapping(value = "/v1/orders/{order_id}"/*, produces = {"application/toString", "application/json"}*/) public Order getOrder(@PathVariable("order_id") @NotBlank String orderId){ Order order = new Order(); BigDecimal total = new BigDecimal(-1.00, new MathContext(2, RoundingMode.HALF_UP)); if (total.compareTo(BigDecimal.ZERO) &lt;= 0){ throw ServiceException.badRequest(ResultCode.INVALID_PARAMETER, "Total is less than zero!"); order.setOrderId(orderId); order.setTotal(total); return order; }复制代码 五. 参考疯狂Java讲义(第十章 - 异常处理)JAVA核心知识点整理原文地址https://www.cnblogs.com/YaoFrankie/p/11440626.html

Python源码学习Schedule

Python源码学习Schedule上一篇《一个简单的Python调度器》介绍了一个简单的Python调度器的使用,后来我翻阅了一下它的源码,惊奇的发现核心库才一个文件,代码量短短700行不到。这是绝佳的学习材料。让我喜出望外的是这个库的作者竟然就是我最近阅读的一本书《Python Tricks》的作者!现在就让我们看看大神的实现思路。 0x00 准备项目地址 https://github.com/dbader/schedule 将代码checkout到本地 PyCharm+venv+Python3 0x01 用法这个在上一篇也介绍过了,非常简单 import schedule 定义需要执行的方法 def job(): print("a simple scheduler in python.") 设置调度的参数,这里是每2秒执行一次 schedule.every(2).seconds.do(job) if name == '__main__': while True: schedule.run_pending() a simple scheduler in python.a simple scheduler in python.a simple scheduler in python....这个库的文档也很详细,可以浏览 https://schedule.readthedocs.io/ 了解库的大概用法 0x02 项目结构(venv) schedule git:(master) tree -L 2....├── requirements-dev.txt├── schedule│ └── __init__.py├── setup.py├── test_schedule.py├── tox.ini└── venv ├── bin ├── include ├── lib ├── pip-selfcheck.json └── pyvenv.cfg 8 directories, 18 filesschedule目录下就一个__init__.py文件,这是我们需要重点学习的地方。setup.py文件是发布项目的配置文件test_schedule.py是单元测试文件,一开始除了看文档外,也可以从单元测试中入手,了解这个库的使用requirements-dev.txt 开发环境的依赖库文件,如果核心的库是不需要第三方的依赖的,但是单元测试需要venv是我checkout后创建的,原本的项目是没有的0x03 schedule我们知道__init__.py是定义Python包必需的文件。在这个文件中定义方法、类都可以在使用import命令时导入到工程项目中,然后使用。 schedule 源码以下是schedule会用到的模块,都是Python内部的模块。 import collectionsimport datetimeimport functoolsimport loggingimport randomimport reimport time logger = logging.getLogger('schedule')然后定义了一个日志打印工具实例 接着是定义了该模块的3个异常类的结构体系,是由Exception派生出来的,分别是ScheduleError、ScheduleValueError和IntervalError class ScheduleError(Exception): """Base schedule exception""" class ScheduleValueError(ScheduleError): """Base schedule value error""" class IntervalError(ScheduleValueError): """An improper interval was used""" 还定义了一个CancelJob的类,用于取消调度器的继续执行 class CancelJob(object): Can be returned from a job to unschedule itself. 例如在自定义的需要被调度方法中返回这个CancelJob类就可以实现一次性的任务 定义需要执行的方法 def job(): print("a simple scheduler in python.") # 返回CancelJob可以停止调度器的后续执行 return schedule.CancelJob 接着就是这个库的两个核心类Scheduler和Job。 class Scheduler(object): Objects instantiated by the :class:`Scheduler &lt;Scheduler&gt;` are factories to create jobs, keep record of scheduled jobs and handle their execution. class Job(object): A periodic job as used by :class:`Scheduler`. :param interval: A quantity of a certain time unit :param scheduler: The :class:`Scheduler &lt;Scheduler&gt;` instance that this job will register itself with once it has been fully configured in :meth:`Job.do()`. Every job runs at a given fixed time interval that is defined by: * a :meth:`time unit &lt;Job.second&gt;` * a quantity of `time units` defined by `interval` A job is usually created and returned by :meth:`Scheduler.every` method, which also defines its `interval`. Scheduler是调度器的实现类,它负责调度任务(job)的创建和执行。 Job则是对需要执行任务的抽象。 这两个类是这个库的核心,后面我们还会看到详细的分析。接下来就是默认调度器default_scheduler和任务列表jobs的创建。 The following methods are shortcuts for not having to create a Scheduler instance: : Default :class:Scheduler &lt;Scheduler&gt; object default_scheduler = Scheduler() : Default :class:Jobs &lt;Job&gt; list jobs = default_scheduler.jobs # todo: should this be a copy, e.g. jobs()?在执行import schedule后,就默认创建了default_scheduler。而Scheduler的构造方法为 def __init__(self): self.jobs = [] 在执行初始化时,调度器就创建了一个空的任务列表。 在文件的最后定义了一些链式调用的方法,使用起来也是非常人性化的,值得学习。这里的方法都定义在模块下,而且都是封装了default_scheduler实例的调用。 def every(interval=1): """Calls :meth:`every &lt;Scheduler.every&gt;` on the :data:`default scheduler instance &lt;default_scheduler&gt;`. return default_scheduler.every(interval) def run_pending(): """Calls :meth:`run_pending &lt;Scheduler.run_pending&gt;` on the :data:`default scheduler instance &lt;default_scheduler&gt;`. default_scheduler.run_pending() def run_all(delay_seconds=0): """Calls :meth:`run_all &lt;Scheduler.run_all&gt;` on the :data:`default scheduler instance &lt;default_scheduler&gt;`. default_scheduler.run_all(delay_seconds=delay_seconds) def clear(tag=None): """Calls :meth:`clear &lt;Scheduler.clear&gt;` on the :data:`default scheduler instance &lt;default_scheduler&gt;`. default_scheduler.clear(tag) def cancel_job(job): """Calls :meth:`cancel_job &lt;Scheduler.cancel_job&gt;` on the :data:`default scheduler instance &lt;default_scheduler&gt;`. default_scheduler.cancel_job(job) def next_run(): """Calls :meth:`next_run &lt;Scheduler.next_run&gt;` on the :data:`default scheduler instance &lt;default_scheduler&gt;`. return default_scheduler.next_run def idle_seconds(): """Calls :meth:`idle_seconds &lt;Scheduler.idle_seconds&gt;` on the :data:`default scheduler instance &lt;default_scheduler&gt;`. return default_scheduler.idle_seconds 我们看下入口方法run_pending(),从本文一开头的Demo可以知道这个是启动调度器的方法。这里它执行了default_scheduler中的方法。 default_scheduler.run_pending()所以我们就把目光定位到Scheduler类的相应方法 def run_pending(self): Run all jobs that are scheduled to run. Please note that it is *intended behavior that run_pending() does not run missed jobs*. For example, if you've registered a job that should run every minute and you only call run_pending() in one hour increments then your job won't be run 60 times in between but only once. runnable_jobs = (job for job in self.jobs if job.should_run) for job in sorted(runnable_jobs): self._run_job(job) 这个方法中首先从jobs列表将需要执行的任务过滤后放在runnable_jobs列表,然后将其排序后顺序执行内部的_run_job(job)方法 def _run_job(self, job): ret = job.run() if isinstance(ret, CancelJob) or ret is CancelJob: self.cancel_job(job) 在_run_job方法中就调用了job类中的run方法,并根据返回值判断是否需要取消任务。 这时候我们要看下Job类的实现逻辑。 首先我们要看下Job是什么时候创建的。还是从Demo中的代码入手 schedule.every(2).seconds.do(job)这里先执行了schedule.every()方法 def every(interval=1): """Calls :meth:`every &lt;Scheduler.every&gt;` on the :data:`default scheduler instance &lt;default_scheduler&gt;`. return default_scheduler.every(interval) 这个方法就是scheduler类中的every方法 def every(self, interval=1): Schedule a new periodic job. :param interval: A quantity of a certain time unit :return: An unconfigured :class:`Job &lt;Job&gt;` job = Job(interval, self) return job 在这里创建了一个任务job,并将参数interval和scheduler实例传入到构造方法中,最后返回job实例用于实现链式调用。 跳转到Job的构造方法 def __init__(self, interval, scheduler=None): self.interval = interval # pause interval * unit between runs self.latest = None # upper limit to the interval self.job_func = None # the job job_func to run self.unit = None # time units, e.g. 'minutes', 'hours', ... self.at_time = None # optional time at which this job runs self.last_run = None # datetime of the last run self.next_run = None # datetime of the next run self.period = None # timedelta between runs, only valid for self.start_day = None # Specific day of the week to start on self.tags = set() # unique set of tags for the job self.scheduler = scheduler # scheduler to register with 主要初始化了间隔时间配置、需要执行的方法、调度器各种时间单位等。 执行every方法之后又调用了seconds这个属性方法 @propertydef seconds(self): self.unit = 'seconds' return self 设置了时间单位,这个设置秒,当然还有其它类似的属性方法minutes、hours、days等等。 最后就是执行了do方法 def do(self, job_func, args, *kwargs): Specifies the job_func that should be called every time the job runs. Any additional arguments are passed on to job_func when the job runs. :param job_func: The function to be scheduled :return: The invoked job instance self.job_func = functools.partial(job_func, *args, **kwargs) functools.update_wrapper(self.job_func, job_func) except AttributeError: # job_funcs already wrapped by functools.partial won't have # __name__, __module__ or __doc__ and the update_wrapper() # call will fail. self._schedule_next_run() self.scheduler.jobs.append(self) return self 在这里使用functools工具的中的偏函数partial将我们自定义的方法封装成可调用的对象 然后就调用_schedule_next_run方法,它主要是对时间的解析,按照时间对job排序,我觉得这个方法是本项目中的技术点,逻辑也是稍微复杂一丢丢,仔细阅读就可以看懂,主要是对时间datetime的使用。由于篇幅,这里就不再贴出代码。 这里就完成了任务job的添加。然后在调用run_pending方法中就可以让任务执行。 0x04 总结一下schedule库定义两个核心类Scheduler和Job。在导入包时就默认创建一个Scheduler对象,并初始化任务列表。schedule模块提供了链式调用的接口,在配置schedule参数时,就会创建任务对象job,并会将job添加到任务列表中,最后在执行run_pending方法时,就会调用我们自定义的方法。这个库的核心思想是使用面向对象方法,对事物能够准确地抽象,它总体的逻辑并不复杂,是学习源码很不错的范例。 0x05 学习资料https://github.com/dbader/schedulehttps://schedule.readthedocs.io关于我一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。Github:https://github.com/hylinux1024微信公众号:终身开发者(angrycode)原文地址https://www.cnblogs.com/angrycode/p/11433283.html

Windows10下载mysql详解

Windows10下载mysql详解 mysql版本分为企业版(Enterprise)和社区版(Community),其中社区办是通过GPL协议授权的开源软件,可以免费使用,而企业版是需要收费的商业软件。 mysql官网 https://www.mysql.com/ ,进去点击 download 在这里插入图片描述 下拉滚动到最后,看到 MySQL Community Edition (GPL),点击 在这里插入图片描述 进去以后点击download 在这里插入图片描述 进入页面,滚动至最下面,看到的版本为8.0.17最新版本。一般下载的版本都为5.5,5.6,点击下图中的 Looking for previous GA versions?即可查找到以前的版本 在这里插入图片描述 在这里插入图片描述 一般选择对应的位数在这里插入图片描述 在这里插入图片描述 最后会弹出个需要注册用户才能下载,直接点击下图即可不用注册 在这里插入图片描述 ZIP Archive 安装包是下载安装源码包安装,而 MSI Installer下载的是安装程序,两者都行。 ZIP Archive安装步骤: 先解压压缩包到指定的目录上,如下例: 在这里插入图片描述 解压至 D:\mysql5.6 打开 此电脑,进入到C盘的Windows文件夹 在这里插入图片描述 搜索 system32,搜到到点击进入文件夹 在这里插入图片描述 在system32的文件夹下搜索cmd 在这里插入图片描述 找到 cmd.exe 在这里插入图片描述 选中右键以管理员的身份运行 切换到安装目录的bin,输入命令 cd D:\mysql5.7\bin 再输入以下命令开始安装 mysqld -install 出现Service successfully installed 说明安装成功了 原文地址https://www.cnblogs.com/xdr630/p/11427255.html

HTML5实时语音通话聊天,MP3压缩传输3KB每秒

HTML5实时语音通话聊天,MP3压缩传输3KB每秒目录 一、把玩方法二、技术特性(1)数据传输(2)音频采集和编码(3)音频实时接收和播放三、应用场景自从Recorder H5 GitHub开源库优化后,对边录边转码成小语音片段文件实时上传服务器这种操作支持非常良好,因此以前不太好支持的H5语音通话已经有了更好的突破空间。因此花了两晚时间打造了一个H5语音通话聊天的demo。 欢迎在线把玩:https://xiangyuecn.github.io/Recorder/ webrtc 一、把玩方法准备局域网内两台设备(Peer A、Peer B)用最新版本浏览器(demo未适配低版本)分别打开demo页面(也可以是同一浏览器打开两个标签)勾选页面中的H5版语音通话聊天,在Peer A中点击新建连接把Peer A的本机信手动复制传输给Peer B,粘贴到远程信息中,并点击确定连接把Peer B自动生成的本机信息手动复制传输给Peer A,粘贴到远程信息中,并点击确定连接双方P2P连接已建立,使用页面上方的录音功能,随时开启录音,音频数据会实时发送给对方局域网H5版对讲机 二、技术特性(1)数据传输github demo中考虑到减少对服务器的依赖,因此采用了WebRTC P2P传输功能,无需任何服务器支持即可实现局域网内的两个设备之间互相连接,连接代码也算简单。有服务器支持可能就要逆天了,不过代码也会更复杂。 如果正式使用,可能不太会考虑使用WebRTC,用WebSocket通过服务器进行转发可能是最佳的选择。 WebRTC局域网P2P连接要点(实际代码其实差不多,只不过多做了点兼容): /Peer A(本机)**/var peerA=new RTCPeerConnection(null,null) //开启会话,等待远程连接peerA.createOffer().then(function(offer){ peerA.setLocalDescription(offer); peerAOffer=offer; var peerAICEList=[......] //通过peerA.onicecandidate监听获得所有的ICE连接信息候选项,如果有多个网络适配器,就会有多个候选 //创建连接通道对象,A端通过这个来进行数据发送var peerAChannel=peerA.createDataChannel("RTC Test"); /Peer B(远程)**/var peerB=new RTCPeerConnection(null,null) //连接到Peer ApeerB.setRemoteDescription(peerAOffer); //开启应答会话,等待Peer A确认连接peerB.createAnswer().then(function(answer){ peerB.setLocalDescription(answer); peerBAnswer=answer; //把Peer A的连接点都添加进去peerB.addIceCandidate(......peerAICEList) var peerBICEList=[......] //通过peerB.onicecandidate监听获得所有的ICE连接信息候选项,如果有多个网络适配器,就会有多个候选 var peerBChannel=... //通过peerB.ondatachannel得到连接通道对象,B端通过这个来进行数据发送 /最终完成连接*///连接到Peer BpeerA.setRemoteDescription(peerBAnswer); //把Peer B的连接点都添加进去peerA.addIceCandidate(......peerBICEList) /*peerA peerB分别等待peerA/BChannel.onopen回调即完成P2P连接,然后通过监听peerA/BChannel.onmessage获得对方发送的信息,通过peerA/BChannel.send(data) 发送数据。*/(2)音频采集和编码由于是在我的Recorder库中新加的demo,因此音频采集和编码都是现成的,Recorder库有好的兼容性和稳定性,因此节省了最大头的工作量。 编码最佳使用MP3格式,因为此格式已优化了实时编码性能,可做到边录边转码,16kbps 16khz的情况下可做到2kb每秒的文件大小,音质还可以,实时传输时为3kb每秒,15分钟大概3M的流量。 用wav格式也可以,不过此格式编码出来的数据量太大,16位 16khz接近50kb每秒的实时传输数据,15分钟要37M多流量。其他格式由于暂未对实时编码进行优化,使用中会导致明显卡顿。 降噪、静音检测等高级功能是没有的,毕竟是非专业人员 要求高点可以,但不要超出范围太多啦。 (3)音频实时接收和播放接收到一个音频片段后,本应该是立即播放的,但由于编码、网络传输导致的延迟,可能上个片段还未播放完(甚至未开始播放),因此需要缓冲处理。 因为存在缓冲,就需要进行实时同步处理,如果缓冲内积压了过多的音频片段,会导致语音播放滞后太多,因此需要适当进行对数据进行丢弃,实测发现网络正常、设备性能靠谱的情况下基本没有丢弃的数据。 然后就是播放了,本应是播完一个就播下一个,测试发现这是不靠谱的。因为结束一个片段后再开始播放下一个发出声音,这个过程会中断比较长时间,明显感觉得出来中间存在短暂停顿。因此必须在片段未播完时准备好下一个片段的播放,并且提前开始播放,达到抹掉中间的停顿。 我写了两个播放方式: 实时解码播放双Audio轮换播放最开始用一个Audio停顿感太明显,因此用两个Audio轮换抹掉中间的停顿,但发现不同格式Auido播放差异巨大,播放wav非常流畅,但播放mp3还是存在停顿(后面用解码的发现是得到的PCM时长变长了,导致事件触发会出现误差,为什么会变长?怪异)。 因此后面写了一个解码然后再播放,mp3这次终于能正常连续播放了,wav格式和双Audio的播放差异不大。实时解码里面也用到了双Audio中的技巧,其实也是用到了两个BufferSource进行类似的轮换操作,以抹掉两个片段间的停顿。 不过最终播放效果还是不够好,音质变差了点,并且多了点噪音。如果有现成的播放代码拿过来用就就好了。 三、应用场景数据传输改成WebSocket,做个仿微信语音通话H5版还是可以的(受限于Recorder浏览器支持)局域网H5版对讲机(前端玩具)......没有想到完。原文地址https://www.cnblogs.com/xiangyuecn/p/11422704.html

如何在搜索引擎中推广网站? 创建一个网站后,只有一个无聊的人可以等待,直到他的网站爬到百度,360和其他搜索引擎的第一个位置。 或者您认为门户网站的访问者数量会自动增加吗?所以要确保即使是最酷的内容也无法带领你走向成功。需要不断努力和一些措施,以帮助促进门户网站。特别是在当今竞争激烈和生存的斗争中。所以继续吧。 推广网站的方法请记住,仅仅购买域名,选择最佳主机并在门户网站上填写有价值的信息是不够的。您需要不断推广您的项目。否则,你会发现一个迷人的失败。 互联网上充斥着各种文章,为推广网络资源提供建议。但是,作为一项规则,它们具有一般性质,没有明确的结构,或根本不适用于您的项目。你必须了解什么是适合你的。为此,请查看以下信息。 如何增加网站流量?您是否已经知道新门户网站的推广费用是由网络工作室专业人员掌控的?当然 毕竟,这就是为什么你正在阅读我们的文章,并希望找到免费推广你的创作的方法。好吧,我们会帮助你。 所以。有几种方法可以帮助您避免严重的开支并增加网站流量: 原创内容符合页面内容查询(相关性)在大型搜索引擎中注册关于项目设计的质量工作在目录,公告板中注册在社交网络中创建群组。所有这些都涉及外部或内部优化,这对年轻的网站很重要。如果您不小心参考优化,那么请确保成功将绕过您。你会破坏你的网站。 在互联网上搜索产品和服务的每个用户或客户都发现了大量竞争公司。你需要告诉他,你应该得到关注,会给他带来好处和好处。更难以与搜索引擎达到类似的效果。毕竟,潜在客户通过他们来到您的网站。因此,我们建议您高度重视Web资源的自我推销问题。 搜索引擎的原理不要以为网站推广的过程是瞬间完成的。你将不得不花费大量的时间和精力来取得好成绩。但值得你付出努力。 选择实现目标的方式,您需要了解搜索引擎的工作方式。标准流程如下所示: 在搜索表单中,我们输入我们需要的信息(所谓的关键字,关键词)。这意味着您需要通过关键字来推广门户网站。定期更新和改进的搜索引擎的算法选择那些最符合用户请求的页面。简而言之,搜索引擎正在寻找相关页面。搜索引擎的运作过程非常困难。现在最好处理案例的细微之处,以便为每个案例正确优化您的网站。 请记住,搜索引擎只会在特定用户群体(目标受众)感兴趣的结果中提供高质量的材料。在形成用户问题之前,他们会仔细分析各种因素,以确保他们此特定网站对用户来说是有趣且必要的。 百度搜索引擎,该系统的算法有多达一千个影响搜索结果的因素。这是一个直接的证据,如果您在推广门户网站时考虑到最大因素数,那么您将有更多机会在问题的前十行。 请记住以下几点:在搜索引擎中推广门户网站时,您应该遵循最重要的因素。然后你会成功。 内部网站优化通过在网站页面上发布高质量的材料,选择正确的内部优化途径,您将对您的业务成功有50%的肯定。 考虑以下参数: 独特,实用和高品质的内容;搜索引擎优化;结构清晰;直观导航;主管链接页面;有效代码(不要被带走);质量和页面的最佳设计。内容优化使用该网站,请注意其内容 – 内容。信息应该是95-100%的唯一信息。这些规则的例外是适用的公式,商品的特征,名称,着名人物的引用。原则上,它们不可能是唯一的。 成功推广您的项目的关键将是任何搜索引擎都喜欢的独特文本。否则,添加到Web资源文章的页面,来自其他来源的图像,您将无法实现积极的动态。 要检查文本的唯一性。 搜索引擎优化(SEO)定义搜索查询的初步列表,以便为他们优化您网站的页面。如果忽略这一要求,该项目将注定无法进入目标受众,这可能来自外部。如果没有这一点,正在推广的网站的行为指标将会恶化,这将影响最终结果。 要优化年轻项目的页面,您应该遵守以下规则来开发和构建数据: 添加标题(页面标题)。它将增加资源的价值,通过其内容和关键词吸引百度蜘蛛和访客。请务必在直接输入中使用关键字。提供内容H1 – H6或第六级的标题。它们还必须包含直接和间接输入的“密钥”。请记住,H1只能在页面上应用一次。在文本中添加关键字(直接,间接和稀释条目)。使用标记来标记将引起访问者注意的词,这些词将增加密钥的重要性。获得信息性数据,将以300字以上的文字,图像,照片和其他元素来表示。导航简单,结构方便在存在可理解导航的结构化页面的情况下,表达了一种经过深思熟虑的发布有用信息的方案。如果一个人来探望你,并且在第一秒钟内无法处理门户的结构,那么请确保他将很快离开你的修道院。同样,也有搜索引擎。 要发布任何数据,您必须首先考虑如何更好地组织它们。我们建议选择站点的树结构,其中包含部分,子部分,产品目录和所选类别。为访问者提供在页面之间导航,访问联系信息,价目表和其他重要信息的便利。理想的项目结构提供以下内容:一个人按照链接,只需点击三次即可找到他想要的内容。 导航是指行为因素,可帮助用户浏览网站,从而改善项目在搜索结果中的位置。 有能力重新链接页面如果您亲自参与门户网站的促销和内部优化,请关注这一点。如果您正确进行内部链接,将向您提供案例的成功。它在Web资源的页面之间分配权重,允许您根据站点的内部功能向上移动结果的位置。 请记住,这项工作需要花费大量的时间和精力,但它可以节省您为网络工作室的服务付费的费用。这些工作的结果将是改进的外部优化。 有效的代码和时尚的网站设计基本的事实是,升级的门户网站应该为用户设计有吸引力,并且其加载应该在短时间内进行。许多Web项目的下载速度都是蹩脚的,因此用户无需等待页面打开(超过3秒),只需将它们留下即可。在这种情况下,搜索蜘蛛会拒绝它们,并且搜索引擎中站点的位置会急剧下降。 对于大多数用户而言,项目的最佳设计是一个有吸引力的视图,而不是针对一小群人。您的任务是实现网站设计的普遍性,确保其推广成功。 外部项目优化适当注意内部优化,不要忘记外部。其有效性在很大程度上取决于之前开展的活动。如果你还没有完成“内部工作”,那么购买链接是没有意义的。你不会达到预期的效果。 通过外部优化,我们的意思是获取正在推广的项目页面的链接。如果您正在寻找计划放置锚点的门户网站,那么请注意它们的质量。 购买链接要获取升级门户的链接,您可以使用以下两种方式: 免费。这将花费大量的时间和劳动力,从而降低其有效性。 支付额外费用。只需支付少量费用,您就可以选择资源需求。我们只说有正确的临时和永久链接需要购买。为此,请考虑以下参数: 所选网站的主题应与您的主题相似;带链接的页面内容应该是提供信息的;它应该接近单声道较少的链路(一个,最多三个;用户应易于查看链接的位置;链接应该是同质的;锚点必须不同(直接和间接输入键);用户应定期访问链接所在的页面;必须索引带有链接的页面;是否存在发布锚点的页面的传入链接;高页面排名。注意链接本身文本的开发。它应包含促销关键字。建议创建几个不同的文本。请记住,锚点的吸引力将允许您增加站点的流量(出勤率)。 另外一件事:你需要一直购买付费链接,增加每批新产品的参考质量。不要一次拿很多链接。否则,您将冒险进入将采取行动的搜索引擎的视图。 其他外部优化选项当然,链接很好,但还不够。要宣传您自己的项目,您需要在免费的主题和其他目录中注册。但主题更好。您需要在注册后添加项目,以指示唯一信息。 热门搜索引擎也提供注册。流行的百度,360将允许索引该网站并将其显示在搜索结果中。 为了推广一个年轻的网站,有很多方法: 在虚拟公告板上发布广告;制作一份通讯,说明您的项目数据;自动注册目录等要宣传您的网站,您可以使用社交书签。在这种情况下,将在其上运行Web资源以获得指向项目的免费链接。 但所有这些选择都很费力,需要很多时间。 结论我们保证可以自己取得好成绩并在线推广您的项目。主要是负责任地处理这个问题,当然,要详细研究我们描述的每个要点。否则,您可能会因项目效率低下而导致错误,需要几个月甚至几年才能纠正错误。 好吧,如果你懒得打扰,那么你应该联系我们排名第一团队的专业人士。 选择权归你的!