C#使用es

Elasticsearch简介

Elasticsearch (ES)是一个基于Apache Lucene(TM)的开源搜索引擎,无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。

但是,Lucene只是一个库。想要发挥其强大的作用,你需使用C#将其集成到你的应用中。Lucene非常复杂,你需要深入的了解检索相关知识来理解它是如何工作的。
Elasticsearch是使用Java编写并使用Lucene来建立索引并实现搜索功能,但是它的目的是通过简单连贯的RESTful API让全文搜索变得简单并隐藏Lucene的复杂性。
不过,Elasticsearch不仅仅是Lucene和全文搜索引擎,它还提供:

  • 分布式的实时文件存储,每个字段都被索引并可被搜索
  • 实时分析的分布式搜索引擎
  • 可以扩展到上百台服务器,处理PB级结构化或非结构化数据

而且,所有的这些功能被集成到一台服务器,你的应用可以通过简单的RESTful API、各种语言的客户端甚至命令行与之交互。上手Elasticsearch非常简单,它提供了许多合理的缺省值,并对初学者隐藏了复杂的搜索引擎理论。它开箱即用(安装即可使用),只需很少的学习既可在生产环境中使用。Elasticsearch在Apache 2 license下许可使用,可以免费下载、使用和修改。
随着知识的积累,你可以根据不同的问题领域定制Elasticsearch的高级特性,这一切都是可配置的,并且配置非常灵活。

以上内容来自 [百度百科]

关于ES详细概念见:http://88250.b3log.org/full-text-search-elasticsearch#b3_solo_h3_0

 

使用C#操作ES

NEST是一个高层的客户端,可以映射所有请求和响应对象,拥有一个强类型查询DSL(领域特定语言),并且可以使用.net的特性比如协变、Auto Mapping Of POCOs,NEST内部使用的依然是Elasticsearch.Net客户端。elasticsearch.net(NEST)客户端提供了强类型查询DSL,方便用户使用,源码下载

一、如何安装NEST

打开VS的工具菜单,通过NuGet包管理器控制台,输入以下命令安装NEST

Install-Package NEST

安装后引用了以下三个DLL

Elasticsearch.Net.dll(2.4.4)
Nest.dll(2.4.4)
Newtonsoft.Json.dll(9.0版本)

二、链接elasticsearch

你可以通过单个节点或者指定多个节点使用连接池链接到Elasticsearch集群,使用连接池要比单个节点链接到Elasticsearch更有优势,比如支持负载均衡、故障转移等。

通过单点链接:

1 var node = new Uri("http://myserver:9200");
2 var settings = new ConnectionSettings(node);
3 var client = new ElasticClient(settings);

 

通过连接池链接:

复制代码
 1 var nodes = new Uri[]
 2 {
 3     new Uri("http://myserver1:9200"),
 4     new Uri("http://myserver2:9200"),
 5     new Uri("http://myserver3:9200")
 6 };
 7 
 8 var pool = new StaticConnectionPool(nodes);
 9 var settings = new ConnectionSettings(pool);
10 var client = new ElasticClient(settings);
复制代码

 

 NEST Index

为了知道请求需要操作哪个索引,Elasticsearch API期望收到一个或多个索引名称作为请求的一部分。

一、指定索引

1、可以通过ConnectionSettings使用.DefaultIndex(),来指定默认索引。当一个请求里没有指定具体索引时,NEST将请求默认索引。

1 var settings = new ConnectionSettings()
2     .DefaultIndex("defaultindex");

2、可以通过ConnectionSettings使用.MapDefaultTypeIndices(),来指定被映射为CLR类型的索引。

1 var settings = new ConnectionSettings()
2     .MapDefaultTypeIndices(m => m
3         .Add(typeof(Project), "projects")
4     );

 

注意:通过.MapDefaultTypeIndices()指定索引的优先级要高于通过.DefaultIndex()指定索引,并且更适合简单对象(POCO)

3、另外还可以显示的为请求指定索引名称,例如:

1 var response = client.Index(student, s=>s.Index("db_test"));
2 var result = client.Search<Student>(s => s.Index("db_test"));
3 var result = client.Delete<Student>(null, s => s.Index("db_test"));
4 ……

 

注意:当现实的为请求指定索引名称时,这个优先级是最高的,高于以上两种方式指定的索引。

4、一些Elasticsearch API(比如query)可以采用一个、多个索引名称或者使用_all特殊标志发送请求,请求NEST上的多个或者所有节点

复制代码
 1 //请求单一节点
 2 var singleString = Nest.Indices.Index("db_studnet");
 3 var singleTyped = Nest.Indices.Index<Student>();
 4 
 5 ISearchRequest singleStringRequest = new SearchDescriptor<Student>().Index(singleString);
 6 ISearchRequest singleTypedRequest = new SearchDescriptor<Student>().Index(singleTyped);
 7 
 8 //请求多个节点
 9 var manyStrings = Nest.Indices.Index("db_studnet", "db_other_student");
10 var manyTypes = Nest.Indices.Index<Student>().And<OtherStudent>();
11 
12 ISearchRequest manyStringRequest = new SearchDescriptor<Student>().Index(manyStrings);
13 ISearchRequest manyTypedRequest = new SearchDescriptor<Student>().Index(manyTypes);
14 
15 //请求所有节点
16 var indicesAll = Nest.Indices.All;
17 var allIndices = Nest.Indices.AllIndices;
18 
19 ISearchRequest indicesAllRequest = new SearchDescriptor<Student>().Index(indicesAll);
20 ISearchRequest allIndicesRequest = new SearchDescriptor<Student>().Index(allIndices);
复制代码

 

二、创建索引

Elasticsearch API允许你创建索引的同时对索引进行配置,例如:

1 var descriptor = new CreateIndexDescriptor("db_student")
2     .Settings(s => s.NumberOfShards(5).NumberOfReplicas(1));
3 
4 client.CreateIndex(descriptor);

 

这里指定了该索引的分片数为5、副本数为1。

三、删除索引

Elasticsearch API允许你删除索引,例如:

1 var descriptor = new DeleteIndexDescriptor("db_student").Index("db_student");
2 
3 client.DeleteIndex(descriptor)

 

这里制定了要删除的索引名称“db_student”,以下为更多删除用例:

1 //删除指定索引所在节点下的所有索引
2 var descriptor = new DeleteIndexDescriptor("db_student").AllIndices();

 

 NEST Mapping

NEST提供了多种映射方法,这里介绍下通过Attribute自定义映射。

一、简单实现

1、定义业务需要的POCO,并指定需要的Attribute

复制代码
 1 [ElasticsearchType(Name = "student")]
 2 public class Student
 3 {
 4     [Nest.String(Index = FieldIndexOption.NotAnalyzed)]
 5     public string Id { get; set; }
 6 
 7     [Nest.String(Analyzer = "standard")]
 8     public string Name { get; set; }
 9 
10     [Nest.String(Analyzer = "standard")]
11     public string Description { get; set; }
12 
13     public DateTime DateTime { get; set; }
14 }
复制代码

 

2、接着我们通过.AutoMap()来实现映射

复制代码
1 var descriptor = new CreateIndexDescriptor("db_student")
2     .Settings(s => s.NumberOfShards(5).NumberOfReplicas(1))
3     .Mappings(ms => ms
4         .Map<Student>(m => m.AutoMap())
5     );
6 
7 client.CreateIndex(descriptor);
复制代码

 

注意:通过.Properties()可以重写通过Attribute定义的映射

二、Attribute介绍

1、StringAttribute

属性名 值类型 描述
Analyzer string 分析器名称,值包含standard、simple、whitespace、stop、keyward、pattern、language、snowball、custom等,查看分析器更多信息请点击Elasticsearch Analyzers
Boost double 加权值,值越大得分越高
NullValue string 插入文档时,如果数据为NULL时的默认值
Index FieldIndexOption 是否使用分析器,默认使用FieldIndexOption.Analyzed,禁止使用分析器FieldIndexOption.NotAnalyzed

2、NumberAttribute

属性名 值类型 描述
type NumberType 构造函数参数,指定当前属性的类型,NumberType.Default、Float、Double、Integer、Long、Short、Byte
Boost double 加权值,值越大得分越高
NullValue double 插入文档时,如果数据为NULL时的默认值

3、BooleanAttribute

属性名 值类型 描述
Boost double 加权值,值越大得分越高
NullValue double 插入文档时,如果数据为NULL时的默认值

4、DateAttribute

属性名 值类型 描述
Boost double 加权值,值越大得分越高
NullValue string 插入文档时,如果数据为NULL时的默认值
Format string

5、ObjectAttribute

属性名 值类型 描述
type string/Type 构造函数参数,指定当前属性的类型T
Dynamic DynamicMapping

 NEST Search

NEST提供了支持Lambda链式query DLS(领域特定语言)方式,以下是简单实现及各个query的简述。

一、简单实现

1、定义SearchDescriptor,方便项目中复杂业务的实现

1 var query = new Nest.SearchDescriptor<Models.ESObject>();
2 
3 var result = client.Search<Student>(x => query)

2、检索title和content中包含key,并且作者不等于“wenli”的文档

复制代码
 1 query.Query(q =>
 2     q.Bool(b =>
 3         b.Must(m =>
 4             m.MultiMatch(t => t.Fields(f => f.Field(obj => obj.Title).Field(obj => obj.Content)).Query(key))
 5         )
 6         .MustNot(m =>
 7             m.QueryString(t => t.Fields(f => f.Field(obj => obj.Author)).Query("wenli"))
 8         )
 9     )
10 );
复制代码
 

注意:

如果Elasticsearch使用默认分词,Title和Content的attribute为[Nest.String(Analyzer = “standard”)]

如果Elasticsearch使用的是IK分词,Title和Content的attribute为[Nest.String(Analyzer = “ikmaxword”)]或者[Nest.String(Analyzer = “ik_smart”)]

Author的attribute为[Nest.String(Index = FieldIndexOption.NotAnalyzed)],禁止使用分析器

3、过滤作者等于“wenli”的文档

query.PostFilter(x => x.Term(t => t.Field(obj => obj.Author).Value("wenli")));

4、过滤作者等于“wenli”或者等于“yswenli”的文档,匹配多个作者中间用空格隔开

1 query.PostFilter(x => x.QueryString(t => t.Fields(f => f.Field(obj => obj.Author)).Query("wenli yswenli")));

5、过滤数量在1~100之间的文档

1 query.PostFilter(x => x.Range(t => t.Field(obj => obj.Number).GreaterThanOrEquals(1).LessThanOrEquals(100)));

 

6、排序,按照得分倒叙排列

1 query.Sort(x => x.Field("_score", Nest.SortOrder.Descending));

 

7、定义高亮样式及字段

复制代码
1 query.Highlight(h => h
2     .PreTags("<b>")
3     .PostTags("</b>")
4     .Fields(
5         f => f.Field(obj => obj.Title),
6         f => f.Field(obj => obj.Content),
7         f => f.Field("_all")
8     )
9 );
复制代码

 

8、拼装查询内容,整理数据,方便前段调用

复制代码
 1 var list = result.Hits.Select(c => new Models.ESObject()
 2 {
 3     Id = c.Source.Id,
 4     Title = c.Highlights == null ? c.Source.Title : c.Highlights.Keys.Contains("title") ? string.Join("", c.Highlights["title"].Highlights) : c.Source.Title, //高亮显示的内容,一条记录中出现了几次
 5     Content = c.Highlights == null ? c.Source.Content : c.Highlights.Keys.Contains("content") ? string.Join("", c.Highlights["content"].Highlights) : c.Source.Content, //高亮显示的内容,一条记录中出现了几次
 6     Author = c.Source.Author,
 7     Number = c.Source.Number,
 8     IsDisplay = c.Source.IsDisplay,
 9     Tags = c.Source.Tags,
10     Comments = c.Source.Comments,
11     DateTime = c.Source.DateTime,
12 })
复制代码

 

二、query DSL介绍

 elasticsearch.net Document

文档操作包含添加/更新文档、局部更新文档、删除文档及对应的批量操作文档方法。

一、添加/更新文档及批量操作

添加/更新单一文档

1 Client.Index(student);

 

批量添加/更新文档

1 var list = new List<Student>();
2 
3 client.IndexMany<Student>(list);

 

二、局部更新单一文档及批量操作

局部更新单一文档

1 client.Update<Student, object>("002", upt => upt.Doc(new { Name = "wenli" }));

 

局部更新批量文档

复制代码
var ids = new List<string>() { "002" };

var bulkQuest = new BulkRequest() { Operations = new List<IBulkOperation>() };

foreach (var v in ids)
{
    var operation = new BulkUpdateOperation<Student, object>(v);

    operation.Doc = new { Name = "wenli" };

    bulkQuest.Operations.Add(operation);
}

var result = client.Bulk(bulkQuest);
复制代码

 

三、删除文档及批量操作

删除单一文档

1 client.Delete<Student>("001");

 

批量删除文档

复制代码
 1 var ids = new List<string>() { "001", "002" };
 2 
 3 var bulkQuest = new BulkRequest() { Operations = new List<IBulkOperation>() };
 4 
 5 foreach (var v in ids)
 6 {
 7     bulkQuest.Operations.Add(new BulkDeleteOperation<Student>(v));
 8 }
 9 
10 var result = client.Bulk(bulkQuest);
复制代码

 

 

转载请标明本文来源:http://www.cnblogs.com/yswenli/
更多内容欢迎我的的github:https://github.com/yswenli
如果发现本文有什么问题和任何建议,也随时欢迎交流~

MySQL在并发场景下的问题及解决思路

1、背景

    对于数据库系统来说在多用户并发条件下提高并发性的同时又要保证数据的一致性一直是数据库系统追求的目标,既要满足大量并发访问的需求又必须保证在此条件下数据的安全,为了满足这一目标大多数数据库通过锁和事务机制来实现,MySQL数据库也不例外。尽管如此我们仍然会在业务开发过程中遇到各种各样的疑难问题,本文将以案例的方式演示常见的并发问题并分析解决思路。

2、表锁导致的慢查询的问题

首先我们看一个简单案例,根据ID查询一条用户信息:

mysql> select * from user where id=6;

这个表的记录总数为3条,但却执行了13秒。

出现这种问题我们首先想到的是看看当前MySQL进程状态:

从进程上可以看出select语句是在等待一个表锁,那么这个表锁又是什么查询产生的呢?这个结果中并没有显示直接的关联关系,但我们可以推测多半是那条update语句产生的(因为进程中没有其他可疑的SQL),为了印证我们的猜测,先检查一下user表结构:

果然user表使用了MyISAM存储引擎,MyISAM在执行操作前会产生表锁,操作完成再自动解锁。如果操作是写操作,则表锁类型为写锁,如果操作是读操作则表锁类型为读锁。正如和你理解的一样写锁将阻塞其他操作(包括读和写),这使得所有操作变为串行;而读锁情况下读-读操作可以并行,但读-写操作仍然是串行。以下示例演示了显式指定了表锁(读锁),读-读并行,读-写串行的情况。

显式开启/关闭表锁,使用lock table user read/write; unlock tables;

session1:

session2:

可以看到会话1启用表锁(读锁)执行读操作,这时会话2可以并行执行读操作,但写操作被阻塞。接着看:

session1:

session2:

当session1执行解锁后,seesion2则立刻开始执行写操作,即读-写串行。

总结:

到此我们把问题的原因基本分析清楚,总结一下——MyISAM存储引擎执行操作时会产生表锁,将影响其他用户对该表的操作,如果表锁是写锁,则会导致其他用户操作串行,如果是读锁则其他用户的读操作可以并行。所以有时我们遇到某个简单的查询花了很长时间,看看是不是这种情况。

解决办法:

1)、尽量不用MyISAM存储引擎,在MySQL8.0版本中已经去掉了所有的MyISAM存储引擎的表,推荐使用InnoDB存储引擎。

2)、如果一定要用MyISAM存储引擎,减少写操作的时间;

3、线上修改表结构有哪些风险?

如果有一天业务系统需要增大一个字段长度,能否在线上直接修改呢?在回答这个问题前,我们先来看一个案例:

以上语句尝试修改user表的name字段长度,语句被阻塞。按照惯例,我们检查一下当前进程:

从进程可以看出alter语句在等待一个元数据锁,而这个元数据锁很可能是上面这条select语句引起的,事实正是如此。在执行DML(select、update、delete、insert)操作时,会对表增加一个元数据锁,这个元数据锁是为了保证在查询期间表结构不会被修改,因此上面的alter语句会被阻塞。那么如果执行顺序相反,先执行alter语句,再执行DML语句呢?DML语句会被阻塞吗?例如我正在线上环境修改表结构,线上的DML语句会被阻塞吗?答案是:不确定。

在MySQL5.6开始提供了online ddl功能,允许一些DDL语句和DML语句并发,在当前5.7版本对online ddl又有了增强,这使得大部分DDL操作可以在线进行。详见:https://dev.mysql.com/doc/refman/5.7/en/innodb-create-index-overview.html

所以对于特定场景执行DDL过程中,DML是否会被阻塞需要视场景而定。

总结:通过这个例子我们对元数据锁和online ddl有了一个基本的认识,如果我们在业务开发过程中有在线修改表结构的需求,可以参考以下方案:

1、尽量在业务量小的时间段进行;

2、查看官方文档,确认要做的表修改可以和DML并发,不会阻塞线上业务;

3、推荐使用percona公司的pt-online-schema-change工具,该工具被官方的online ddl更为强大,它的基本原理是:通过insert… select…语句进行一次全量拷贝,通过触发器记录表结构变更过程中产生的增量,从而达到表结构变更的目的。

例如要对A表进行变更,主要步骤为:

创建目的表结构的空表,A_new;
在A表上创建触发器,包括增、删、改触发器;
通过insert…select…limit N 语句分片拷贝数据到目的表
Copy完成后,将A_new表rename到A表。

4、一个死锁问题的分析

在线上环境下死锁的问题偶有发生,死锁是因为两个或多个事务相互等待对方释放锁,导致事务永远无法终止的情况。为了分析问题,我们下面将模拟一个简单死锁的情况,然后从中总结出一些分析思路。

演示环境:MySQL5.7.20 事务隔离级别:RR

表user:

CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(300) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8

下面演示事务1、事务2工作的情况:

事务1
事务2
事务监控
T1 begin;

Query OK, 0 rows affected (0.00 sec)

begin;

Query OK, 0 rows affected (0.00 sec)

T2 select * from user where id=3 for update;

+—-+——+——+
| id | name | age |
+—-+——+——+
| 3 | sun | 20 |
+—-+——+——+
1 row in set (0.00 sec)

select * from user where id=4 for update;

+—-+——+——+
| id | name | age |
+—-+——+——+
| 4 | zhou | 21 |
+—-+——+——+
1 row in set (0.00 sec)

select * from information_schema.INNODB_TRX;

通过查询元数据库innodb事务表,监控到当前运行事务数为2,即事务1、事务2。

T3 update user set name=’haha’ where id=4;

因为id=4的记录已被事务2加上行锁,该语句将阻塞

监控到当前运行事务数为2。
T4 阻塞状态 update user set name=’hehe’ where id=3;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

id=3的记录已被事务1加上行锁,而本事务持有id=4的记录行锁,此时InnoDB存储引擎检查出死锁,本事务被回滚。

事务2被回滚,事务1仍在运行中,监控当前运行事务数为1。
T5 Query OK, 1 row affected (20.91 sec)
Rows matched: 1 Changed: 1 Warnings: 0

由于事务2被回滚,原来阻塞的update语句被继续执行。

监控当前运行事务数为1。
T6 commit;

Query OK, 0 rows affected (0.00 sec)

事务1已提交、事务2已回滚,监控当前运行事务数为0。

这是一个简单的死锁场景,事务1、事务2彼此等待对方释放锁,InnoDB存储引擎检测到死锁发生,让事务2回滚,这使得事务1不再等待事务B的锁,从而能够继续执行。那么InnoDB存储引擎是如何检测到死锁的呢?为了弄明白这个问题,我们先检查此时InnoDB的状态:

show engine innodb status\G

————————
LATEST DETECTED DEADLOCK
————————
2018-01-14 12:17:13 0x70000f1cc000
*** (1) TRANSACTION:
TRANSACTION 5120, ACTIVE 17 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 10, OS thread handle 123145556967424, query id 2764 localhost root updating
update user set name=’haha’ where id=4
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 94 page no 3 n bits 80 index PRIMARY of table `test`.`user` trx id 5120 lock_mode X locks rec but not gap waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 0000000013fa; asc ;;
2: len 7; hex 520000060129a6; asc R ) ;;
3: len 4; hex 68616861; asc haha;;
4: len 4; hex 80000015; asc ;;

*** (2) TRANSACTION:
TRANSACTION 5121, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 11, OS thread handle 123145555853312, query id 2765 localhost root updating
update user set name=’hehe’ where id=3
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 94 page no 3 n bits 80 index PRIMARY of table `test`.`user` trx id 5121 lock_mode X locks rec but not gap
Record lock, heap no 5 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 0000000013fa; asc ;;
2: len 7; hex 520000060129a6; asc R ) ;;
3: len 4; hex 68616861; asc haha;;
4: len 4; hex 80000015; asc ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 94 page no 3 n bits 80 index PRIMARY of table `test`.`user` trx id 5121 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 0000000013fe; asc ;;
2: len 7; hex 5500000156012f; asc U V /;;
3: len 4; hex 68656865; asc hehe;;
4: len 4; hex 80000014; asc ;;

*** WE ROLL BACK TRANSACTION (2)

InnoDB状态有很多指标,这里我们截取死锁相关的信息,可以看出InnoDB可以输出最近出现的死锁信息,其实很多死锁监控工具也是基于此功能开发的。

在死锁信息中,显示了两个事务等待锁的相关信息(蓝色代表事务1、绿色代表事务2),重点关注:WAITING FOR THIS LOCK TO BE GRANTED和HOLDS THE LOCK(S)。

WAITING FOR THIS LOCK TO BE GRANTED表示当前事务正在等待的锁信息,从输出结果看出事务1正在等待heap no为5的行锁,事务2正在等待 heap no为7的行锁;

HOLDS THE LOCK(S):表示当前事务持有的锁信息,从输出结果看出事务2持有heap no为5行锁。

从输出结果看出,最后InnoDB回滚了事务2。

那么InnoDB是如何检查出死锁的呢?

我们想到最简单方法是假如一个事务正在等待一个锁,如果等待时间超过了设定的阈值,那么该事务操作失败,这就避免了多个事务彼此长等待的情况。参数innodb_lock_wait_timeout正是用来设置这个锁等待时间的。

如果按照这个方法,解决死锁是需要时间的(即等待超过innodb_lock_wait_timeout设定的阈值),这种方法稍显被动而且影响系统性能,InnoDB存储引擎提供一个更好的算法来解决死锁问题,wait-for graph算法。简单的说,当出现多个事务开始彼此等待时,启用wait-for graph算法,该算法判定为死锁后立即回滚其中一个事务,死锁被解除。该方法的好处是:检查更为主动,等待时间短。

下面是wait-for graph算法的基本原理:

为了便于理解,我们把死锁看做4辆车彼此阻塞的场景:

                

4辆车看做4个事务,彼此等待对方的锁,造成死锁。wait-for graph算法原理是把事务作为节点,事务之间的锁等待关系,用有向边表示,例如事务A等待事务B的锁,就从节点A画一条有向边到节点B,这样如果A、B、C、D构成的有向图,形成了环,则判断为死锁。这就是wait-for graph算法的基本原理。

总结:

1、如果我们业务开发中出现死锁如何检查出?刚才已经介绍了通过监控InnoDB状态可以得出,你可以做一个小工具把死锁的记录收集起来,便于事后查看。

2、如果出现死锁,业务系统应该如何应对?从上文我们可以看到当InnoDB检查出死锁后,对客户端报出一个Deadlock found when trying to get lock; try restarting transaction信息,并且回滚该事务,应用端需要针对该信息,做事务重启的工作,并保存现场日志事后做进一步分析,避免下次死锁的产生。

5、锁等待问题的分析

在业务开发中死锁的出现概率较小,但锁等待出现的概率较大,锁等待是因为一个事务长时间占用锁资源,而其他事务一直等待前个事务释放锁。

事务1
事务2
事务监控
T1 begin;

Query OK, 0 rows affected (0.00 sec)

begin;

Query OK, 0 rows affected (0.00 sec)

T2 select * from user where id=3 for update;

+—-+——+——+
| id | name | age |
+—-+——+——+
| 3 | sun | 20 |
+—-+——+——+
1 row in set (0.00 sec)

其他查询操作 select * from information_schema.INNODB_TRX;

通过查询元数据库innodb事务表,监控到当前运行事务数为2,即事务1、事务2。

T3  其他查询操作  update user set name=’hehe’ where id=3;

因为id=3的记录被事务1加上行锁,所以该语句将阻塞(即锁等待)

 监控到当前运行事务数为2。
T4 其他查询操作 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

锁等待时间超过阈值,操作失败。注意:此时事务2并没有回滚。

监控到当前运行事务数为2。
T5 commit; 事务1已提交,事务2未提交,监控到当前运行事务数为1。

从上述可知事务1长时间持有id=3的行锁,事务2产生锁等待,等待时间超过innodb_lock_wait_timeout后操作中断,但事务并没有回滚。如果我们业务开发中遇到锁等待,不仅会影响性能,还会给你的业务流程提出挑战,因为你的业务端需要对锁等待的情况做适应的逻辑处理,是重试操作还是回滚事务。

在MySQL元数据表中有对事务、锁等待的信息进行收集,例如information_schema数据库下的INNODB_LOCKS、INNODB_TRX、INNODB_LOCK_WAITS,你可以通过这些表观察你的业务系统锁等待的情况。你也可以用一下语句方便的查询事务和锁等待的关联关系:

SELECT     r.trx_id waiting_trx_id,     r.trx_mysql_thread_id waiting_thread,     r.trx_query wating_query,     b.trx_id blocking_trx_id,     b.trx_mysql_thread_id blocking_thread,     b.trx_query blocking_query FROM     information_schema.innodb_lock_waits w         INNER JOIN     information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id         INNER JOIN     information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

结果:

waiting_trx_id: 5132
waiting_thread: 11
wating_query: update user set name=’hehe’ where id=3
blocking_trx_id: 5133
blocking_thread: 10
blocking_query: NULL

总结:

1、请对你的业务系统做锁等待的监控,这有助于你了解当前数据库锁情况,以及为你优化业务程序提供帮助;

2、业务系统中应该对锁等待超时的情况做合适的逻辑判断。

6、小结

    本文通过几个简单的示例介绍了我们常用的几种MySQL并发问题,并尝试得出针对这些问题我们排查的思路。文中涉及事务、表锁、元数据锁、行锁,但引起并发问题的远远不止这些,例如还有事务隔离级别、GAP锁等。真实的并发问题可能多而复杂,但排查思路和方法却是可以复用,在本文中我们使用了show processlist;show engine innodb status;以及查询元数据表的方法来排查发现问题,如果问题涉及到了复制,还需要借助master/slave监控来协助。

 

参考资料:

姜承尧《InnoDB存储引擎》

李宏哲 杨挺 《MySQL排查指南》

何登成 http://hedengcheng.com

 

========================我是广告=======================

美团点评在成都、北京、上海招聘初中高级java后台、前端工程师,月薪20-50k,需要内推的可以发邮件给我:360841519@qq.com

有赞API网关实践

一、API网关简介

随着移动互联网的兴起、开放合作思维的盛行,不同终端和第三方开发者都需要大量的接入企业核心业务能力,此时各业务系统将会面临同一系列的问题,例如:如何让调用方快速接入、如何让业务方安全地对外开放能力,如何应对和控制业务洪峰调用等等。于是就诞生了一个隔离企业内部业务系统和外部系统调用的屏障 – API网关,它负责在上层抽象出各业务系统需要的通用功能,例如:鉴权、限流、ACL、降级等。另外随着近年来微服务的流行,API网关已经成为一个微服务架构中的标配组件。

二、有赞API网关简介

有赞API网关目前承载着微商城、零售、微小店、餐饮、美业、AppSDK、部分PC、三方开发者等多个业务的调用,每天有着亿级别的流量。

有赞后端服务最开始是由PHP搭建,随着整个技术体系的升级,后面逐步从PHP迁移到Java体系。在API网关设计之初主要支持Dubbo、Http两种协议。迁移过程中,我们发现部分服务需要通过RPC方式调用PHP服务,于是我们(公司)基于Dubbo开发了一个新的框架Nova,兼容Dubbo调用,同时支持调用PHP服务。于是网关也支持了新的Nova协议,这样就有Dubbo、Http、Nova三种协议。

随着业务的不断发展,业务服务化速度加快,网关面临各类新的需求。例如回调类型的API接入,这种API不需要鉴权,只需要一个限流服务,路由到后端服务即可;另外还有参数、返回值的转换需求也不断到来,这期间我们快速迭代满足新的需求。而在这个过程中我们也走了很多弯路,例如API的规范,在最开始规范意识比较笼统,导致返回值在对外暴露时出现了不统一的情况,后续做SDK自动化的时候比较棘手,经过不断的约束开发者,最终做到了统一。

三、架构与设计

1. 网关架构

部署架构图

网关的调用方主要包括微商城、微小店、零售等App应用,以及三方开发者和部分PC业务。通过LVS做负载均衡,后端Tengine实现反向代理,网关应用调用到实际的业务集群

应用架构图

网关核心由Pipe链构成,每个Pipe负责一块功能,同时使用缓存、异步等特性提升并发及性能

线程模型图

网关采用Jetty部署,调用采用Http协议,请求由容器线程池处理(容器开启了Servlet3.0异步,提升了较大的吞吐量),之后分发到应用线程池异步处理。应用线程池在设计之初考虑不同的任务执行可能会出现耗时不一的情况,所以将任务分别拆分到不同的线程池,以提高不同类型任务的并发度,如图分为CommonGroup, ExecutionGroup, ResultGroup

CommonGroup执行通用任务,ExecutionGroup执行多协议路由及调用任务,ResultGroup执行结果处理任务(包含异常)

网关业务生态图

网关生态主要包含控制台、网关核心、网关统计与监控
控制台主要对API生命周期进行管理,以及ACL、流量管控等功能;
网关核心主要处理API调用,包含鉴权、限流、路由、协议转换等功能;
统计与监控模块主要完成API调用的统计以及对店铺、三方的一些报表统计,同时提供监控功能和报警功能

2. 网关核心设计

2.1 异步

我们使用Jetty容器来部署应用,并开启Servlet3.0的异步特性,由于网关业务本身就是调用大量业务接口,因此IO操作会比较频繁,使用该特性能较大提升网关整体并发能力及吞吐量。另外我们在内部处理开启多组线程池进行异步处理,以异步回调的方式通知任务完成,进一步提升并发量

image

2.2 二级缓存

为了进一步提升网关的性能,我们增加了一层分布式缓存(借用Codis实现),将一些不经常变更的API元数据缓存下来,这样不仅减少了应用和DB的交互次数,还加快了读取效率。我们同时考虑到Codis在极端情况下存在不稳定因素,因此我们在本地再次做了本地缓存,这样的读取可以从ms级别降低到ns级别。为了实现多台机器的本地缓存一致性,我们使用了ZK监听节点变化来更新各机器本地缓存

image

2.3 链式处理

在设计网关的时候,我们采用责任链模式来实现网关的核心处理流程,将每个处理逻辑看成一个Pipe,每个Pipe按照预先设定的顺序先后执行,与开源的Zuul 1.x类似,我们也采用了PRPE模式(Pre、Routing、Post、Error),在我们这里Pre分为PrePipe、RateLimitPipe、AuthPipe、AclPipe、FlowSepPipe,这些Pipe对数据进行预处理、限流、鉴权、访问控制、分流,并将过滤后的Context向下传递;Routing分为DubboPipe、HttpPipe,这些Pipe分别处理Dubbo协议、Http协议路由及调用;Post为ResultPipe,处理正常返回值以及统计打点,Error为ErrorPipe,处理异常场景

image

2.4 线程池隔离

Jetty容器线程池(QTP)负责接收Http请求,之后交由应用线程池CommonGroup,ExecutionGroup, ResultGroup,通用的操作将会被放到CommonGroup线程池执行,执行真实调用的被放到ExecutionGroup,结果处理放到ResultGroup。这样部分Pipe之间线程隔离,通常前置Pipe处理都比较快,所以共享线程池即可,真实调用通常比较耗时,因此我们放到独立的线程池,同时结果处理也存在一些运算,因此也放到独立线程池

image

2.5 平滑限流

最早我们采用了简单的分布式缓存(Codis)计数实现限流,以IP、API维度构建Key进行累加,这种限流方式实现简单,但是不能做到连续时间段内平滑限流。例如针对某个API每分钟限流100次,第1秒发起20次,第二秒发起30次,第3秒发起40次,这样的限流波动比较大,因此我们决定将其改进。经过调研我们最终选择了令牌桶限流,令牌桶限流相比于漏桶限流能适应闲置较长时段后的尖峰调用,同时消除了简单计数器限流带来的短时间内流量不均的问题。目前网关支持IP、店铺、API、应用ID和三方ID等多个维度的限流,也支持各维度的自由组合限流,可以很容易扩展出新的维度

image

2.6 熔断降级

由于我们经常遇到调用后端接口超时,或者异常的情况,后端服务无法立即恢复,这种情况下再将请求发到后端已没有意义。于是我们使用Hystrix进行熔断降级处理。Hystrix支持线程池和信号量2种模式的隔离方案,网关的业务场景是多API和API分组,每个API都可能路由到不同后端服务,如果我们对API或者API分组做线程池隔离,就会产生大量的线程,所以我们选择了信号量做隔离。我们为每个API提供一个降级配置,用户可以选择自己配置的API在达到多少错误率时进行熔断降级。
引入Hystrix后,Hystrix会对每个API做统计,包括总量、正确率、QPS等指标,同时会产生大量事件,当API很多的时候,这些指标和事件会占用大量内存,导致更加频繁的YoungGC,这对应用性能产生了一定的影响,不过整体的收益还是不错的

另外有赞内部也开发了一个基于Hystrix的服务熔断平台(Tesla),平台在可视化、易用性、扩展性上面均有较大程度的提升;后续网关会考虑熔断模块的实现基于服务熔断平台,以提供更好的服务

image

2.7 分流

有赞内部存在多种协议类型的后端服务,最原始的服务是PHP开发,后面逐渐迁移到Java,很早一部分API是由PHP暴露的,后续为了能做灰度迁移到Java,我们做了分流,将老的PHP接口的流量按照一定的比例分发到新的Java接口上

3. 控制台

除了核心功能的调用外,网关还需要支持内部用户(下称业务方)快速配置接口暴露给开发者。 控制台主要职责包括:快速配置API、一站式测试API、一键发布API,自动化文档生成,自动化SDK生成

  • 快速配置API:这块我们主要是按照对外、对内来进行配置,业务方将自己要对外公开的名称、参数编辑好,再通过对内映射将对外参数映射到内部服务的接口里面

image

  • 一站式测试API:API配置完成后,为了能让业务方快速测试,我们做了一站式获取鉴权值,参数值自动保存,做到一站式测试

image

  • 一键发布API:在完成配置和测试后,API就可以直接发布,这个时候选择对应环境的注册中心或者服务域名即可

image

  • 自动化文档生成:我们针对文档这块做了文档中心,对内部用户,他们只需要到平台来搜索即可,对外部用户,可以在有赞云官网查看或者在控制台直接导出pdf文件给用户

image

  • 自动化SDK生成:对于开发者来说,接入一个平台必然少不了SDK,我们针对多语言做了自动化SDK生成,当用户的接口发布成功后,我们会监听到有新的接口,这时会触发自动编译(Java)SDK的模块,将新接口打包成新版本的压缩包,供开发者使用;如果编译失败(Java)则不会替换老的压缩包,我们会发送报警给相应的开发者,让其调整不规范的地方

image

4. 数据统计

为了让业务方能在上线后了解自己的接口的运行状况,我们做了API相关的统计。我们通过在核心模块里面打日志,利用rsyslog采集数据到Kafka,然后从Kafka消费进行统计,之后回流到数据库供在线查询

除此之外,我们为每个商家做了他们授权的服务商调用接口的统计。这块功能的实现,我们通过Storm从Kafka实时消费,并实时统计落HBase,每天凌晨将前一天的数据同步到Hive进行统计并回流到数据库

image

5. 报警监控

业务方API上线后,除了查看统计外,当API出问题时,还需要及时发现。我们针对这块做了API报警功能。用户在平台配置自己的API的报警,这里我们主要支持基于错误数或RT维度的报警。
我们实时地从Kafka消费API调用日志,如果发现某个API的RT或者错误次数超过配置的报警阈值,则会立即触发报警

image

四、实践总结

1. 规范

在网关上暴露的API很多,如何让这些API按照统一的标准对外暴露,让开发者能够低门槛快速接入是网关需要思考的问题

网关规范主要是对API的命名、入参(公用入参、业务入参)、内部服务返回值、错误码(公用错误码、业务错误码)、出参(公用出参、业务出参),进行规范

在我们的实践过程中,总结了以下规范:

  • 命名规范:youzan.[业务线(可选)].[应用名].[动作].[版本],例如:youzan.item.create.3.0.0
  • 入参规范:要求全部小写,组合单词以下划线分隔,例如:title, item_id;入参如果是一个结构体,要求以json字符串传入,并且json中的key必须小写并且以下划线分隔
  • 出参规范:要求全部小写,组合单词以下划线分隔,例如:page_num, total_count;如果参数为结构体,结构体里面的key必须小写且以下划线分隔
  • 错误码规范:我们做了统一的错误码,例如系统级错误码51xxx,业务错误码50000,详情信息由msg显示;业务级错误码由业务方自行定义,同时约束每个业务方的错误码范围
  • 服务返回值规范:针对不同的业务方,每个API可能会有不同的业务错误,我们需要将这部分业务级错误展示给开发者,因此我们约定返回值需要按照一个POJO类型(包含code, msg, data)来返回,对于code为200,我们认为正常返回,否则认为是业务错误,将返回值包装为错误结果

2. 发布

  • 我们将API划分到3个环境,分别为测试环境、预发环境、生产环境。API的创建、编辑必须在测试环境进行,测试完成后,可以将API发布到预发环境,之后再从预发环境发布到生产环境,这样可以保持三个环境的API数据一致。好处是:一方面可以让测试开发能在测试环境进行自动化验证,另一方面可以防止用户直接编辑线上接口引发故障

3. 工具化

  • 对于内部用户经常可能需要排查问题,例如OAuth Token里面带的参数,需要经常查询,我们提供工具化的控制台,能让用户方便查询,从而减少答疑量
  • 我们上线后也曾经出现过缓存不一致的情况,为了能快速排查问题,我们做了缓存管理工具,能在图形化界面上查看本地缓存以及Codis的缓存,可以进行对比找出差异
  • 为了更好的排查线上问题,我们接入了有赞对比引擎(Replay)平台,该平台能将线上的流量引到预发,帮助开发者更快定位问题

五、踩过的坑

  • Meta区Full GC导致服务无法响应

    现象:应用hung死,调用接口返回503,无法服务

    排查过程:现场dump了内存,GC记录,以及线程运行快照。首先看了GC发现是Full GC,但是不清楚是哪里发生的,看线程运行快照也没发现什么问题。于是在本地用HeapAnalysis分析,堆区没看出什么问题,大对象都是应该占用的;于是查看方法区,通过ClassLoader Analysis发现Fastjson相关的类较多,因此怀疑是class泄露,进一步通过MAT的OQL语法分析,发现是Fastjson在序列化Jetty容器的HttpServletRequest时,为了加快速度于是创建新的类时抛了异常,导致动态创建的类在方法区堆积从而引发Full GC,后续我们也向Fastjson提了相关bug

    解决方案:将序列化HttpServletRequest的代码移除

  • 伪死循环导致CPU 100%

    现象:在有赞双11全链路压测期间,某个业务调用API,导致我们的应用CPU几乎接近100%

    排查过程:经过日志分析,发现该接口存在大量超时,但是从代码没看出特别有问题的地方。于是我们将接口在QA环境模拟调用,用VisualVM连上去,通过抽样器抽样CPU,发现某个方法消耗CPU较高,因此我们迅速定位到源码,发现这段代码主要是执行轮询任务是否完成,如果完成则调用完成回调,如果未完成继续放到队列。再结合之前的环境观察发现大量超时的任务被放到队列,导致任务被取出后,任务仍然是未完成状态,这样会将任务放回队列,这样其实构成了一个死循环

    解决方案:将主动轮询改为异步通知,我们这里是Dubbo调用,Dubbo调用返回的Future实际是一个FutureAdapter,可以获取到里面的ResponseFuture(DefaultFuture),这个类型的Future支持设置Callback,任务完成时会通知到设置的回调

六、未来展望

  1. 业务级资源组隔离。随着业务的不断发展,当业务线较多时,可以将重要的业务分配到更优质的资源组(例如:机器性能、线程池的大小),将一般业务放到普通资源组,这样可以更好的服务不同的业务场景
  2. 更高并发的线程池/IO的优化。随着业务的发展,未来可能会出现更高的并发,需要更精良的线程及IO模型
  3. 更多的协议支持。以后技术的发展,Http2可能会蓬勃发展,这时需要接入Http2的协议

七、结语

有赞网关目前归属有赞共享技术-基础服务中心团队开发和维护;
该团队目前主要分为商品中心、库存中心、物流中心、消息沟通平台、云生态5个小组;
商品/库存/物流中心:通过不断抽象上层业务,完成通用的模型建设;为上层业务方提供高可用的服务,并快速响应多变的业务需求;针对秒杀、洪峰调用、及上层业务多变等需求,三个小组还齐力开发和持续完善着 对比引擎、服务熔断、热点探测等三个通用系统;
消息沟通平台:提供几乎一切消息沟通相关的能力及一套帮助商家与用户联系的多客服系统,每天承载着上亿次调用(短信、apppush、语音、微信、微博、多客服、邮件等通道);
云生态:承担着核心网关的建设和发展(上面的网关应用系统)、三方推送系统、有赞云后台、商业化订购以及App Engine的预研和开发;

目前该团队HC开放,期待有机会与各位共事;(内推邮箱:huangtao@youzan.com)

注,本文作者:有赞网关(黄涛、尹铁夫、叮咚)

知识共享许可协议
如无特殊说明,本文版权归 本文作者及有赞技术团队 所有,采用 署名-非商业性使用 4.0 国际许可协议 进行许可。
转载请注明:来自有赞技术团队博客 http://tech.youzan.com/api-gateway-in-practice/

别慌,不就是跨域么

什么是跨域

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。

同源策略限制了一下行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获取
  • Ajax请求发送不出去

常见的跨域场景

所谓的同源是指,域名、协议、端口均为相同。

http://www.nealyang.cn/index.html 调用 http://www.nealyang.cn/server.php 非跨

http://www.nealyang.cn/index.html 调用 http://www.neal.cn/server.php 跨域,主域不同

http://abc.nealyang.cn/index.html 调用 http://def.neal.cn/server.php 跨域,子域名不同

http://www.nealyang.cn:8080/index.html 调用 http://www.nealyang.cn/server.php 跨域,端口不同

https://www.nealyang.cn/index.html 调用 http://www.nealyang.cn/server.php 跨域,协议不同

localhost 调用 127.0.0.1 跨域

跨域的解决办法

jsonp跨域

jsonp跨域其实也是JavaScript设计模式中的一种代理模式。在html页面中通过相应的标签从不同域名下加载静态资源文件是被浏览器允许的,所以我们可以通过这个“犯罪漏洞”来进行跨域。一般,我们可以动态的创建script标签,再去请求一个带参网址来实现跨域通信

//原生的实现方式

let script = document.createElement(‘script’);

script.src = ‘http://www.nealyang.cn/login?username=Nealyang&callback=callback’;

document.body.appendChild(script);

function callback(res) {

console.log(res);

}

当然,jquery也支持jsonp的实现方式

$.ajax({

url:’http://www.nealyang.cn/login’,

type:’GET’,

dataType:’jsonp’,//请求方式为jsonp

jsonpCallback:’callback’,

data:{

“username”:”Nealyang”

}

})

虽然这种方式非常好用,但是一个最大的缺陷是,只能够实现get请求

document.domain + iframe 跨域

这种跨域的方式最主要的是要求主域名相同。什么是主域名相同呢? www.nealyang.cn aaa.nealyang.cn ba.ad.nealyang.cn 这三个主域名都是nealyang.cn,而主域名不同的就不能用此方法。

假设目前a.nealyang.cn 和 b.nealyang.cn 分别对应指向不同ip的服务器。

a.nealyang.cn 下有一个test.html文件

<!DOCTYPE html>

<html lang=”en”>

<head>

<meta charset=”UTF-8″>

<title>html</title>

<script type=”text/javascript” src = “jquery-1.12.1.js”></script>

</head>

<body>

<div>A页面</div>

<iframe

style = “display : none”

name = “iframe1”

id = “iframe”

src=”http://b.nealyang.cn/1.html” frameborder=”0″></iframe>

<script type=”text/javascript”>

$(function(){

try{

document.domain = “nealyang.cn”

}catch(e){}

$(“#iframe”).load(function(){

var jq = document.getElementById(‘iframe’).contentWindow.$

jq.get(“http://nealyang.cn/test.json”,function(data){

console.log(data);

});

})

})

</script>

</body>

</html>

利用 iframe 加载 其他域下的文件(nealyang.cn/1.html), 同时 document.domain 设置成 nealyang.cn ,当 iframe 加载完毕后就可以获取 nealyang.cn 域下的全局对象, 此时尝试着去请求 nealyang.cn 域名下的 test.json (此时可以请求接口),就会发现数据请求失败了~~ 惊不惊喜,意不意外!!!!!!!

数据请求失败,目的没有达到,自然是还少一步:

<!DOCTYPE html>

<html lang=”en”>

<head>

<meta charset=”UTF-8″>

<title>html</title>

<script type=”text/javascript” src = “jquery-1.12.1.js”></script>

<script type=”text/javascript”>

$(function(){

try{

document.domain = “nealyang.com”

}catch(e){}

})

</script>

</head>

<body>

<div id = “div1”>B页面</div>

</body>

</html>

此时在进行刷新浏览器,就会发现数据这次真的是成功了

window.name + iframe 跨域

window.name属性可设置或者返回存放窗口名称的一个字符串。他的神器之处在于name值在不同页面或者不同域下加载后依旧存在,没有修改就不会发生变化,并且可以存储非常长的name(2MB)

假设index页面请求远端服务器上的数据,我们在该页面下创建iframe标签,该iframe的src指向服务器文件的地址(iframe标签src可以跨域),服务器文件里设置好window.name的值,然后再在index.html里面读取改iframe中的window.name的值。完美~

<body>

<script type=”text/javascript”>

iframe = document.createElement(‘iframe’),

iframe.src = ‘http://localhost:8080/data.php’;

document.body.appendChild(iframe);

iframe.onload = function() {

console.log(iframe.contentWindow.name)

};

</script>

</body>

当然,这样还是不够的。

因为规定如果index.html页面和和该页面里的iframe框架的src如果不同源,则也无法操作框架里的任何东西,所以就取不到iframe框架的name值了,告诉你我们不是一家的,你也休想得到我这里的数据。 既然要同源,那就换个src去指,前面说了无论怎样加载window.name值都不会变化,于是我们在index.html相同目录下,新建了个proxy.html的空页面,修改代码如下:

<body>

<script type=”text/javascript”>

iframe = document.createElement(‘iframe’),

iframe.src = ‘http://localhost:8080/data.php’;

document.body.appendChild(iframe);

iframe.onload = function() {

iframe.src = ‘http://localhost:81/cross-domain/proxy.html’;

console.log(iframe.contentWindow.name)

};

</script>

</body>

理想似乎很美好,在iframe载入过程中,迅速重置iframe.src的指向,使之与index.html同源,那么index页面就能去获取它的name值了!但是现实是残酷的,iframe在现实中的表现是一直不停地刷新, 也很好理解,每次触发onload时间后,重置src,相当于重新载入页面,又触发onload事件,于是就不停地刷新了(但是需要的数据还是能输出的)。修改后代码如下:

<body>

<script type=”text/javascript”>

iframe = document.createElement(‘iframe’);

iframe.style.display = ‘none’;

var state = 0;

iframe.onload = function() {

if(state === 1) {

var data = JSON.parse(iframe.contentWindow.name);

console.log(data);

iframe.contentWindow.document.write(”);

iframe.contentWindow.close();

document.body.removeChild(iframe);

} else if(state === 0) {

state = 1;

iframe.contentWindow.location = ‘http://localhost:81/cross-domain/proxy.html’;

}

};

iframe.src = ‘http://localhost:8080/data.php’;

document.body.appendChild(iframe);

</script>

</body>

所以如上,我们就拿到了服务器返回的数据,但是有几个条件是必不可少的:

  • iframe标签的跨域能力
  • window.names属性值在文档刷新后依然存在的能力

location.hash + iframe 跨域

此跨域方法和上面介绍的比较类似,一样是动态插入一个iframe然后设置其src为服务端地址,而服务端同样输出一端js代码,也同时通过与子窗口之间的通信来完成数据的传输。

关于锚点相信大家都已经知道了,其实就是设置锚点,让文档指定的相应的位置。锚点的设置用a标签,然后href指向要跳转到的id,当然,前提是你得有个滚动条,不然也不好滚动嘛是吧。

而location.hash其实就是url的锚点。比如http://www.nealyang.cn#Nealyang的网址打开后,在控制台输入location.hash就会返回#Nealyang的字段。

基础知识补充完毕,下面我们来说下如何实现跨域

如果index页面要获取远端服务器的数据,动态的插入一个iframe,将iframe的src执行服务器的地址,这时候的top window 和包裹这个iframe的子窗口是不能通信的,因为同源策略,所以改变子窗口的路径就可以了,将数据当做改变后的路径的hash值加载路径上,然后就可以通信了。将数据加在index页面地址的hash上, index页面监听hash的变化,h5的hashchange方法

<body>

<script type=”text/javascript”>

function getData(url, fn) {

var iframe = document.createElement(‘iframe’);

iframe.style.display = ‘none’;

iframe.src = url;

iframe.onload = function() {

fn(iframe.contentWindow.location.hash.substring(1));

window.location.hash = ”;

document.body.removeChild(iframe);

};

document.body.appendChild(iframe);

}

// get data from server

var url = ‘http://localhost:8080/data.php’;

getData(url, function(data) {

var jsondata = JSON.parse(data);

console.log(jsondata.name + ‘ ‘ + jsondata.age);

});

</script>

</body>

补充说明:其实location.hash和window.name都是差不多的,都是利用全局对象属性的方法,然后这两种方法和jsonp也是一样的,就是只能够实现get请求

postMessage跨域

这是由H5提出来的一个炫酷的API,IE8+,chrome,ff都已经支持实现了这个功能。这个功能也是非常的简单,其中包括接受信息的Message时间,和发送信息的postMessage方法。

发送信息的postMessage方法是向外界窗口发送信息

otherWindow.postMessage(message,targetOrigin);

otherWindow指的是目标窗口,也就是要给哪一个window发送消息,是window.frames属性的成员或者是window.open方法创建的窗口。 Message是要发送的消息,类型为String,Object(IE8、9不支持Obj),targetOrigin是限定消息接受范围,不限制就用星号 *

接受信息的message事件

var onmessage = function(event) {

var data = event.data;

var origin = event.origin;

}

if(typeof window.addEventListener != ‘undefined’){

window.addEventListener(‘message’,onmessage,false);

}else if(typeof window.attachEvent != ‘undefined’){

window.attachEvent(‘onmessage’, onmessage);

}

举个栗子

a.html(http://www.nealyang.cn/a.html)

<iframe id=”iframe” src=”http://www.neal.cn/b.html” style=”display:none;”></iframe>

<script>

var iframe = document.getElementById(‘iframe’);

iframe.onload = function() {

var data = {

name: ‘aym’

};

// 向neal传送跨域数据

iframe.contentWindow.postMessage(JSON.stringify(data), ‘http://www.neal.cn’);

};

// 接受domain2返回数据

window.addEventListener(‘message’, function(e) {

alert(‘data from neal —> ‘ + e.data);

}, false);

</script>

b.html(http://www.neal.cn/b.html)

<script>

// 接收domain1的数据

window.addEventListener(‘message’, function(e) {

alert(‘data from nealyang —> ‘ + e.data);

var data = JSON.parse(e.data);

if (data) {

data.number = 16;

// 处理后再发回nealyang

window.parent.postMessage(JSON.stringify(data), ‘http://www.nealyang.cn’);

}

}, false);

</script>

跨域资源共享 CORS

因为是目前主流的跨域解决方案。所以这里多介绍点。

简介

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。IE8+:IE8/9需要使用XDomainRequest对象来支持CORS。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

两种请求

说起来很搞笑,分为两种请求,一种是简单请求,另一种是非简单请求。只要满足下面条件就是简单请求

  • 请求方式为HEAD、POST 或者 GET
  • http头信息不超出一下字段:Accept、Accept-Language 、 Content-Language、 Last-Event-ID、 Content-Type(限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)

为什么要分为简单请求和非简单请求,因为浏览器对这两种请求方式的处理方式是不同的。

简单请求

基本流程

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。 下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1

Origin: http://api.bob.com

Host: api.alice.com

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0

Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。 浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。

注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com

Access-Control-Allow-Credentials: true

Access-Control-Expose-Headers: FooBar

Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头

  • Access-Control-Allow-Origin :该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
  • Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
  • Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

另一方面,开发者必须在AJAX请求中打开withCredentials属性。

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie

xhr.withCredentials = true;

xhr.open(‘post’, ‘http://www.domain2.com:8080/login’, true);

xhr.setRequestHeader(‘Content-Type’, ‘application/x-www-form-urlencoded’);

xhr.send(‘user=admin’);

xhr.onreadystatechange = function() {

if (xhr.readyState == 4 && xhr.status == 200) {

alert(xhr.responseText);

}

};

// jquery

$.ajax({

xhrFields: {

withCredentials: true // 前端设置是否带cookie

},

crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie

});

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。 但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

var url = ‘http://api.alice.com/cors’;

var xhr = new XMLHttpRequest();

xhr.open(‘PUT’, url, true);

xhr.setRequestHeader(‘X-Custom-Header’, ‘value’);

xhr.send();

浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求,要求服务器确认可以这样请求。下面是这个”预检”请求的HTTP头信息。

OPTIONS /cors HTTP/1.1

Origin: http://api.bob.com

Access-Control-Request-Method: PUT

Access-Control-Request-Headers: X-Custom-Header

Host: api.alice.com

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0…

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,”预检”请求的头信息包括两个特殊字段。

  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
预检请求的回应

服务器收到”预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应

HTTP/1.1 200 OK

Date: Mon, 01 Dec 2008 01:15:39 GMT

Server: Apache/2.0.61 (Unix)

Access-Control-Allow-Origin: http://api.bob.com

Access-Control-Allow-Methods: GET, POST, PUT

Access-Control-Allow-Headers: X-Custom-Header

Content-Type: text/html; charset=utf-8

Content-Encoding: gzip

Content-Length: 0

Keep-Alive: timeout=2, max=100

Connection: Keep-Alive

Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

如果浏览器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

服务器回应的其他CORS相关字段如下:

Access-Control-Allow-Methods: GET, POST, PUT

Access-Control-Allow-Headers: X-Custom-Header

Access-Control-Allow-Credentials: true

Access-Control-Max-Age: 1728000

  • Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
  • Access-Control-Allow-Headers:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。
  • Access-Control-Allow-Credentials: 该字段与简单请求时的含义相同。
  • Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
浏览器正常请求回应

一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

PUT /cors HTTP/1.1

Origin: http://api.bob.com

Host: api.alice.com

X-Custom-Header: value

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0…

浏览器的正常CORS请求。上面头信息的Origin字段是浏览器自动添加的。下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.bob.com

Content-Type: text/html; charset=utf-8

Access-Control-Allow-Origin字段是每次回应都必定包含的

结束语

CORS与JSONP的使用目的相同,但是比JSONP更强大。JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

前端代码:

<div>user input:<input type=”text”></div>

<script src=”./socket.io.js”></script>

<script>

var socket = io(‘http://www.domain2.com:8080’);

// 连接成功处理

socket.on(‘connect’, function() {

// 监听服务端消息

socket.on(‘message’, function(msg) {

console.log(‘data from server: —> ‘ + msg);

});

// 监听服务端关闭

socket.on(‘disconnect’, function() {

console.log(‘Server socket has closed.’);

});

});

document.getElementsByTagName(‘input’)[0].onblur = function() {

socket.send(this.value);

};

</script>

node Server

var http = require(‘http’);

var socket = require(‘socket.io’);

// 启http服务

var server = http.createServer(function(req, res) {

res.writeHead(200, {

‘Content-type’: ‘text/html’

});

res.end();

});

server.listen(‘8080’);

console.log(‘Server is running at port 8080…’);

// 监听socket连接

socket.listen(server).on(‘connection’, function(client) {

// 接收信息

client.on(‘message’, function(msg) {

client.send(‘hello:’ + msg);

console.log(‘data from client: —> ‘ + msg);

});

// 断开处理

client.on(‘disconnect’, function() {

console.log(‘Client socket has closed.’);

});

});

node代理跨域

node中间件实现跨域代理,是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

利用node + express + http-proxy-middleware搭建一个proxy服务器

前端代码

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie

xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器

xhr.open(‘get’, ‘http://www.domain1.com:3000/login?user=admin’, true);

xhr.send();

后端代码

var express = require(‘express’);

var proxy = require(‘http-proxy-middleware’);

var app = express();

app.use(‘/’, proxy({

// 代理跨域目标接口

target: ‘http://www.domain2.com:8080’,

changeOrigin: true,

// 修改响应头信息,实现跨域并允许带cookie

onProxyRes: function(proxyRes, req, res) {

res.header(‘Access-Control-Allow-Origin’, ‘http://www.domain1.com’);

res.header(‘Access-Control-Allow-Credentials’, ‘true’);

},

// 修改响应信息中的cookie域名

cookieDomainRewrite: ‘www.domain1.com’ // 可以为false,表示不修改

}));

app.listen(3000);

console.log(‘Proxy server is listen at port 3000…’);

nginx代理跨域

NGINX其实个人没有怎么玩过,所以暂且也就不能误人子弟了,原谅笔者才疏尚浅~ 有机会学习研究再回来补充~~

参考文档

http://www.ruanyifeng.com/blog/2016/04/cors.html

https://segmentfault.com/a/1190000011145364

via  github.com/Nealyang/YOU-SHOULD-KNOW-JS/blob/master/doc/basic_js/JavaScript中的跨域总结.md

Net Core中数据库事务隔离详解——以Dapper和Mysql为例

事务隔离级别

.NET Core中的IDbConnection接口提供了BeginTransaction方法作为执行事务,BeginTransaction方法提供了两个重载,一个不需要参数BeginTransaction()默认事务隔离级别为RepeatableRead;另一个BeginTransaction(IsolationLevel il)可以根据业务需求来修改事务隔离级别。由于Dapper是对IDbConnection的扩展,所以Dapper在执行增删除改查时所有用到的事务需要由外部来定义。事务执行时与数据库之间的交互如下:

2017-12-19-22-43-23

从WireShark抓取的数据包来看程序和数据交互步骤依次是:建立连接-->设置数据库隔离级别-->告诉数据库一个事务开始-->执行数据增删查改-->提交事务-->断开连接

准备工作

准备数据库:Mysql (笔者这里是:MySql 5.7.20 社区版)

创建数据库并创建数据表,创建数据表的脚本如下:


CREATE TABLE `posts` (
  `Id` varchar(255) NOT NULL ,
  `Text` longtext NOT NULL,
  `CreationDate` datetime NOT NULL,
  `LastChangeDate` datetime NOT NULL,
  `Counter1` int(11) DEFAULT NULL,
  `Counter2` int(11) DEFAULT NULL,
  `Counter3` int(11) DEFAULT NULL,
  `Counter4` int(11) DEFAULT NULL,
  `Counter5` int(11) DEFAULT NULL,
  `Counter6` int(11) DEFAULT NULL,
  `Counter7` int(11) DEFAULT NULL,
  `Counter8` int(11) DEFAULT NULL,
  `Counter9` int(11) DEFAULT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

创建.NET Core Domain类:


[Table("Posts")]
public class Post
{
    [Key]
    public string Id { get; set; }
    public string Text { get; set; }
    public DateTime CreationDate { get; set; }
    public DateTime LastChangeDate { get; set; }
    public int? Counter1 { get; set; }
    public int? Counter2 { get; set; }
    public int? Counter3 { get; set; }
    public int? Counter4 { get; set; }
    public int? Counter5 { get; set; }
    public int? Counter6 { get; set; }
    public int? Counter7 { get; set; }
    public int? Counter8 { get; set; }
    public int? Counter9 { get; set; }

}

具体怎样使用Dapper,请看上篇

Read uncommitted 读未提交

允许脏读,即不发布共享锁,也不接受独占锁。意思是:事务A可以读取事务B未提交的数据。

优点:查询速度快

缺点:容易造成脏读,如果事务A在中途回滚

以下为执行脏读的测试代码片断:


public static void RunDirtyRead(IsolationLevel transaction1Level,IsolationLevel transaction2Level)
{
    var id = Guid.NewGuid().ToString();
    using (var connection1 = new MySqlConnection(connStr))
    {
        connection1.Open();
        Console.WriteLine("transaction1 {0} Start",transaction1Level);
        var transaction1 = connection1.BeginTransaction(transaction1Level);
        Console.WriteLine("transaction1 插入数据 Start");
        var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)";
        var detail1 = connection1.Execute(sql,
        new Post
        {
            Id = id,
            Text = Guid.NewGuid().ToString(),
            CreationDate = DateTime.Now,
            LastChangeDate = DateTime.Now
        },
            transaction1);
        Console.WriteLine("transaction1 插入End 返回受影响的行:{0}", detail1);
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2 {0} Start",transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            Console.WriteLine("transaction2 查询数据 Start");
            var result = connection2.QueryFirstOrDefault<Post>("select * from posts where id=@Id", new { id = id }, transaction2);
            //如果result为Null 则程序会报异常
            Console.WriteLine("transaction2 查询结事 返回结果:Id={0},Text={1}", result.Id, result.Text);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End",transaction2Level);
        }
        transaction1.Rollback();
        Console.WriteLine("transaction1 {0} Rollback ",transaction1Level);
    }

}

1、当执行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadUncommitted),即事务1和事务2都设置为ReadUncommitted时结果如下:

2017-12-22-22-06-49

当事务1回滚以后,数据库并没有事务1添加的数据,所以事务2获取的数据是脏数据。

2、当执行RunDirtyRead(IsolationLevel.Serializable,IsolationLevel.ReadUncommitted),即事务1隔离级别为Serializble,事务2的隔离级别设置为ReadUncommitted,结果如下:

2017-12-22-22-07-28

3、当执行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadCommitted);,即事务1隔离级别为ReadUncommitted,事务2的隔离级别为Readcommitted,结果如下:

2017-12-22-22-08-13

结论:当事务2(即取数据事务)隔离级别设置为ReadUncommitted,那么不管事务1隔离级别为哪一种,事务2都能将事务1未提交的数据得到;但是测试结果可以看出当事务2为ReadCommitted则获取不到事务1未提交的数据从而导致程序异常。

Read committed 读取提交内容

这是大多数数据库默认的隔离级别,但是,不是MySQL的默认隔离级别。读取数据时保持共享锁,以避免脏读,但是在事务结束前可以更改数据。

优点:解决了脏读的问题

缺点:一个事务未结束被另一个事务把数据修改后导致两次请求的数据不一致

测试重复读代码片断:


public static void RunRepeatableRead(IsolationLevel transaction1Level, IsolationLevel transaction2Level)
{
    using (var connection1 = new MySqlConnection(connStr))
    {
        connection1.Open();
        var id = "c8de065a-3c71-4273-9a12-98c8955a558d";
        Console.WriteLine("transaction1 {0} Start", transaction1Level);
        var transaction1 = connection1.BeginTransaction(transaction1Level);
        Console.WriteLine("transaction1 第一次查询开始");
        var sql = "select * from posts where id=@Id";
        var detail1 = connection1.QueryFirstOrDefault<Post>(sql, new { Id = id }, transaction1);
        Console.WriteLine("transaction1 第一次查询结束,结果:Id={0},Counter1={1}", detail1.Id, detail1.Counter1);
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2  {0} Start", transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            var updateCounter1=(detail1.Counter1 ?? 0) + 1;
            Console.WriteLine("transaction2  开始修改Id={0}中Counter1的值修改为:{1}", id,updateCounter1);
            var result = connection2.Execute(
                "update posts set Counter1=@Counter1 where id=@Id",
                new { Id = id, Counter1 = updateCounter1 },
                transaction2);
            Console.WriteLine("transaction2 修改完成 返回受影响行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
        Console.WriteLine("transaction1 第二次查询 Start");
        var detail2 = connection1.QueryFirstOrDefault<Post>(sql, new { Id = id }, transaction1);
        Console.WriteLine("transaction1 第二次查询 End 结果:Id={0},Counter1={1}", detail2.Id, detail2.Counter1);
        transaction1.Commit();
        Console.WriteLine("transaction1 {0} End", transaction1Level);
    }
}

在事务1中detail1中得到的Counter1为1,事务2中将Counter1的值修改为2,事务1中detail2得到的Counter1的值也会变为2

下面分几种情况来测试:

1、当事务1和事务2都为ReadCommitted时,结果如下:

2017-12-21-21-29-41

2017-12-22-22-08-38

2、当事务1和事务2隔离级别都为RepeatableRead时,执行结果如下:

2017-12-22-22-08-53

3、当事务1隔离级别为RepeatableRead,事务2隔离级别为ReadCommitted时执行结果如下:

2017-12-22-22-09-09

4、当事务1隔离级别为ReadCommitted,事务2隔离级别为RepeatableRead时执行结果如下:

2017-12-22-22-09-30

结论:当事务1隔离级别为ReadCommitted时数据可重复读,当事务1隔离级别为RepeatableRead时可以不可重复读,不管事务2隔离级别为哪一种不受影响。

注:在RepeatableRead隔离级别下虽然事务1两次获取的数据一致,但是事务2已经是将数据库中的数据进行了修改,如果事务1对该条数据进行修改则会对事务2的数据进行覆盖。

Repeatable read (可重读)

这是MySQL默认的隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行(目标数据行不会被修改)。

优点:解决了不可重复读和脏读问题

缺点:幻读

测试幻读代码

 
public static void RunPhantomRead(IsolationLevel transaction1Level, IsolationLevel transaction2Level)
{
    using (var connection1 = new MySqlConnection(connStr))
    {
        connection1.Open();
        Console.WriteLine("transaction1 {0} Start", transaction1Level);
        var transaction1 = connection1.BeginTransaction(transaction1Level);
        Console.WriteLine("transaction1 第一次查询数据库 Start");
        var detail1 = connection1.Query<Post>("select * from posts").ToList();
        Console.WriteLine("transaction1 第一次查询数据库 End 查询条数:{0}", detail1.Count);
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2 {0} Start", transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            Console.WriteLine("transaction2 执行插入数据 Start");
            var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)";
            var entity = new Post
            {
                Id = Guid.NewGuid().ToString(),
                Text = Guid.NewGuid().ToString(),
                CreationDate = DateTime.Now,
                LastChangeDate = DateTime.Now
            };
            var result = connection2.Execute(sql, entity, transaction2);
            Console.WriteLine("transaction2 执行插入数据 End 返回受影响行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
        Console.WriteLine("transaction1 第二次查询数据库 Start");
        var detail2 = connection1.Query<Post>("select * from posts").ToList();
        Console.WriteLine("transaction1 第二次查询数据库 End 查询条数:{0}", detail2.Count);
        transaction1.Commit();
        Console.WriteLine("transaction1 {0} End", transaction1Level);
    }
}

分别对几种情况进行测试:

1、事务1和事务2隔离级别都为RepeatableRead,结果如下:

2017-12-22-22-09-46

2、事务1和事务2隔离级别都为Serializable,结果如下:

2017-12-22-22-10-02

3、当事务1的隔离级别为Serializable,事务2的隔离级别为RepeatableRead时,执行结果如下:

2017-12-22-22-10-18

4、当事务1的隔离级别为RepeatableRead,事务2的隔离级别为Serializable时,执行结果如下:

2017-12-22-22-10-32

结论:当事务隔离级别为RepeatableRead时虽然两次获取数据条数相同,但是事务2是正常将数据插入到数据库当中的。当事务1隔离级别为Serializable程序异常,原因接下来将会讲到。

Serializable 序列化

这是最高的事务隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。

优点:解决幻读

缺点:在每个读的数据行上都加了共享锁,可能导致大量的超时和锁竞争

当执行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.Serializable)或执行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.RepeatableRead)时代码都会报异常,是因为Serializable隔离级别下强制事务以串行方式执行,由于这里是一个主线程上第一个事务未完时执行了第二个事务,但是第二个事务必须等到第一个事务执行完成后才参执行,所以就会导致程序报超时异常。这里将代码作如下修改:


using (var connection1 = new MySqlConnection(connStr))
{
    connection1.Open();
    Console.WriteLine("transaction1 {0} Start", transaction1Level);
    var transaction1 = connection1.BeginTransaction(transaction1Level);
    Console.WriteLine("transaction1 第一次查询数据库 Start");
    var detail1 = connection1.Query<Post>("select * from posts").ToList();
    Console.WriteLine("transaction1 第一次查询数据库 End 查询条数:{0}", detail1.Count);
    Thread thread = new Thread(new ThreadStart(() =>
    {
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2 {0} Start", transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            Console.WriteLine("transaction2 执行插入数据 Start");
            var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)";
            var entity = new Post
            {
                Id = Guid.NewGuid().ToString(),
                Text = Guid.NewGuid().ToString(),
                CreationDate = DateTime.Now,
                LastChangeDate = DateTime.Now
            };
            var result = connection2.Execute(sql, entity, transaction2);
            Console.WriteLine("transaction2 执行插入数据 End 返回受影响行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
    }));
    thread.Start();
    //为了证明两个事务是串行执行的,这里让主线程睡5秒
    Thread.Sleep(5000);
    Console.WriteLine("transaction1 第二次查询数据库 Start");
    var detail2 = connection1.Query<Post>("select * from posts").ToList();
    Console.WriteLine("transaction1 第二次查询数据库 End 查询条数:{0}", detail2.Count);
    transaction1.Commit();
    Console.WriteLine("transaction1 {0} End", transaction1Level);
}

执行结果如下:

2017-12-22-22-11-02

2017-12-22-22-11-13

结论:当事务1隔离级别为Serializable时对后面的事务的增删改改操作进行强制排序。避免数据出错造成不必要的麻烦。

注:在.NET Core中IsolationLevel枚举值中还提供了另外三种隔离级别:ChaosSnapshotUnspecified由于这种事务隔离级别MySql不支持设置时会报异常:

2017-12-20-21-31-13

总结

本节通过Dapper对MySql中事务的四种隔离级别下进行测试,并且指出事务之间的相互关系和问题以供大家参考。

1、事务1隔离级别为ReadUncommitted时,可以读取其它任何事务隔离级别下未提交的数据

2、事务1隔离级别为ReadCommitted时,不可以读取其它事务未提交的数据,但是允许其它事务对数据表进行查询、添加、修改和删除;并且可以将其它事务增删改重新获取出来。

3、事务1隔离级别为RepeatableRead时,不可以读取其它事务未提交的数据,但是允许其它事务对数据表进行查询、添加、修改和删除;但是其它事务的增删改不影响事务1的查询结果

4、事务1隔离级别为Serializable时,对其它事务对数据库的修改(增删改)强制串行处理。

脏读 重复读 幻读
Read uncommitted
Read committed 不会
Repeatable read 不会 不会
Serializable 不会 不会 不会

作者:xdpie 出处:http://www.cnblogs.com/vipyoumay/p/8134434.html

以上内容有任何错误或不准确的地方请大家指正,不喜勿喷! 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧。

Hadoop和大数据:60款顶级开源工具

说到处理大数据的工具,普通的开源解决方案(尤其是Apache Hadoop)堪称中流砥柱。弗雷斯特调研公司的分析师Mike Gualtieri最近预测,在接下来几年,“100%的大公司”会采用Hadoop。Market Research的一份报告预测,到2011年,Hadoop市场会以58%的年复合增长率(CAGR)高速增长;到2020年,市场产值会超过10亿美元。IBM更是非常看好开源大数据工具,派出了3500名研究人员开发Apache Spark,这个工具是Hadoop生态系统的一部分。

这回我们推出了最新的顶级开源大数据工具排行榜。这个领域最近方兴未艾,许多新项目纷纷启动。许多最知名的项目由Apache基金会管理,与Hadoop密切相关。

请注意:本文不是要搞什么排名;相反,项目按类别加以介绍。与往常一样,要是你知道另外的开源大数据及/或Hadoop工具应该榜上有名,欢迎留言交流。

一、Hadoop相关工具

1. Hadoop

Apache的Hadoop项目已几乎与大数据划上了等号。它不断壮大起来,已成为一个完整的生态系统,众多开源工具面向高度扩展的分布式计算。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://hadoop.apache.org

2. Ambari

作为Hadoop生态系统的一部分,这个Apache项目提供了基于Web的直观界面,可用于配置、管理和监控Hadoop集群。有些开发人员想把Ambari的功能整合到自己的应用程序当中,Ambari也为他们提供了充分利用REST(代表性状态传输协议)的API。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://ambari.apache.org

3. Avro

这个Apache项目提供了数据序列化系统,拥有丰富的数据结构和紧凑格式。模式用JSON来定义,它很容易与动态语言整合起来。

支持的操作系统:与操作系统无关。

相关链接: http://avro.apache.org

4. Cascading

Cascading是一款基于Hadoop的应用程序开发平台。提供商业支持和培训服务。

支持的操作系统:与操作系统无关。

相关链接: http://www.cascading.org/projects/cascading/

5. Chukwa

Chukwa基于Hadoop,可以收集来自大型分布式系统的数据,用于监控。它还含有用于分析和显示数据的工具。

支持的操作系统:Linux和OS X。

相关链接: http://chukwa.apache.org

6. Flume

Flume可以从其他应用程序收集日志数据,然后将这些数据送入到Hadoop。官方网站声称:“它功能强大、具有容错性,还拥有可以调整优化的可靠性机制和许多故障切换及恢复机制。”

支持的操作系统:Linux和OS X。

相关链接: https://cwiki.apache.org/confluence/display/FLUME/Home

7. HBase

HBase是为有数十亿行和数百万列的超大表设计的,这是一种分布式数据库,可以对大数据进行随机性的实时读取/写入访问。它有点类似谷歌的Bigtable,不过基于Hadoop和Hadoop分布式文件系统(HDFS)而建。

支持的操作系统:与操作系统无关。

相关链接: http://hbase.apache.org

8. Hadoop分布式文件系统(HDFS

HDFS是面向Hadoop的文件系统,不过它也可以用作一种独立的分布式文件系统。它基于Java,具有容错性、高度扩展性和高度配置性。

支持的操作系统:Windows、Linux和OS X。

相关链接: https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsUserGuide.html

9. Hive

Apache Hive是面向Hadoop生态系统的数据仓库。它让用户可以使用HiveQL查询和管理大数据,这是一种类似SQL的语言。

支持的操作系统:与操作系统无关。

相关链接: http://hive.apache.org

10. Hivemall

Hivemall结合了面向Hive的多种机器学习算法。它包括诸多高度扩展性算法,可用于数据分类、递归、推荐、k最近邻、异常检测和特征哈希。

支持的操作系统:与操作系统无关。

相关链接: https://github.com/myui/hivemall

11. Mahout

据官方网站声称,Mahout项目的目的是“为迅速构建可扩展、高性能的机器学习应用程序打造一个环境。”它包括用于在Hadoop MapReduce上进行数据挖掘的众多算法,还包括一些面向Scala和Spark环境的新颖算法。

支持的操作系统:与操作系统无关。

相关链接: http://mahout.apache.org

12. MapReduce

作为Hadoop一个不可或缺的部分,MapReduce这种编程模型为处理大型分布式数据集提供了一种方法。它最初是由谷歌开发的,但现在也被本文介绍的另外几个大数据工具所使用,包括CouchDB、MongoDB和Riak。

支持的操作系统:与操作系统无关。

相关链接: http://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html

13. Oozie

这种工作流程调度工具是为了管理Hadoop任务而专门设计的。它能够按照时间或按照数据可用情况触发任务,并与MapReduce、Pig、Hive、Sqoop及其他许多相关工具整合起来。

支持的操作系统:Linux和OS X。

相关链接: http://oozie.apache.org

14. Pig

Apache Pig是一种面向分布式大数据分析的平台。它依赖一种名为Pig Latin的编程语言,拥有简化的并行编程、优化和可扩展性等优点。

支持的操作系统:与操作系统无关。

相关链接: http://pig.apache.org

15. Sqoop

企业经常需要在关系数据库与Hadoop之间传输数据,而Sqoop就是能完成这项任务的一款工具。它可以将数据导入到Hive或HBase,并从Hadoop导出到关系数据库管理系统(RDBMS)。

支持的操作系统:与操作系统无关。

相关链接: http://sqoop.apache.org

16. Spark

作为MapReduce之外的一种选择,Spark是一种数据处理引擎。它声称,用在内存中时,其速度比MapReduce最多快100倍;用在磁盘上时,其速度比MapReduce最多快10倍。它可以与Hadoop和Apache Mesos一起使用,也可以独立使用。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://spark.apache.org

17. Tez

Tez建立在Apache Hadoop YARN的基础上,这是“一种应用程序框架,允许为任务构建一种复杂的有向无环图,以便处理数据。”它让Hive和Pig可以简化复杂的任务,而这些任务原本需要多个步骤才能完成。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://tez.apache.org

18. Zookeeper

这种大数据管理工具自称是“一项集中式服务,可用于维护配置信息、命名、提供分布式同步以及提供群组服务。”它让Hadoop集群里面的节点可以彼此协调。

支持的操作系统:Linux、Windows(只适合开发环境)和OS X(只适合开发环境)。

相关链接: http://zookeeper.apache.org

二、大数据分析平台和工具

19. Disco

Disco最初由诺基亚开发,这是一种分布式计算框架,与Hadoop一样,它也基于MapReduce。它包括一种分布式文件系统以及支持数十亿个键和值的数据库。

支持的操作系统:Linux和OS X。

相关链接: http://discoproject.org

20. HPCC

作为Hadoop之外的一种选择,HPCC这种大数据平台承诺速度非常快,扩展性超强。除了免费社区版外,HPCC Systems还提供收费的企业版、收费模块、培训、咨询及其他服务。

支持的操作系统:Linux。

相关链接: http://hpccsystems.com

21. Lumify

Lumify归Altamira科技公司(以国家安全技术而闻名)所有,这是一种开源大数据整合、分析和可视化平台。你只要在Try.Lumify.io试一下演示版,就能看看它的实际效果。

支持的操作系统:Linux。

相关链接: http://www.jboss.org/infinispan.html

22. Pandas

Pandas项目包括基于Python编程语言的数据结构和数据分析工具。它让企业组织可以将Python用作R之外的一种选择,用于大数据分析项目。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://pandas.pydata.org

23. Storm

Storm现在是一个Apache项目,它提供了实时处理大数据的功能(不像Hadoop只提供批任务处理)。其用户包括推特、美国天气频道、WebMD、阿里巴巴、Yelp、雅虎日本、Spotify、Group、Flipboard及其他许多公司。

支持的操作系统:Linux。

相关链接: https://storm.apache.org

三、数据库/数据仓库

24. Blazegraph

Blazegraph之前名为“Bigdata”,这是一种高度扩展、高性能的数据库。它既有使用开源许可证的版本,也有使用商业许可证的版本。

支持的操作系统:与操作系统无关。

相关链接: http://www.systap.com/bigdata

25. Cassandra

这种NoSQL数据库最初由Facebook开发,现已被1500多家企业组织使用,包括苹果、欧洲原子核研究组织(CERN)、康卡斯特、电子港湾、GitHub、GoDaddy、Hulu、Instagram、Intuit、Netfilx、Reddit及其他机构。它能支持超大规模集群;比如说,苹果部署的Cassandra系统就包括75000多个节点,拥有的数据量超过10 PB。

支持的操作系统:与操作系统无关。

相关链接: http://cassandra.apache.org

26. CouchDB

CouchDB号称是“一款完全拥抱互联网的数据库”,它将数据存储在JSON文档中,这种文档可以通过Web浏览器来查询,并且用JavaScript来处理。它易于使用,在分布式上网络上具有高可用性和高扩展性。

支持的操作系统:Windows、Linux、OS X和安卓。

相关链接: http://couchdb.apache.org

27. FlockDB

由推特开发的FlockDB是一种非常快、扩展性非常好的图形数据库,擅长存储社交网络数据。虽然它仍可用于下载,但是这个项目的开源版已有一段时间没有更新了。

支持的操作系统:与操作系统无关。

相关链接: https://github.com/twitter/flockdb

28. Hibari

这个基于Erlang的项目自称是“一种分布式有序键值存储系统,保证拥有很强的一致性”。它最初是由Gemini Mobile Technologies开发的,现在已被欧洲和亚洲的几家电信运营商所使用。

支持的操作系统:与操作系统无关。

相关链接: http://hibari.github.io/hibari-doc/

29. Hypertable

Hypertable是一种与Hadoop兼容的大数据数据库,承诺性能超高,其用户包括电子港湾、百度、高朋、Yelp及另外许多互联网公司。提供商业支持服务。

支持的操作系统:Linux和OS X。

相关链接: http://hypertable.org

30. Impala

Cloudera声称,基于SQL的Impala数据库是“面向Apache Hadoop的领先的开源分析数据库”。它可以作为一款独立产品来下载,又是Cloudera的商业大数据产品的一部分。

支持的操作系统:Linux和OS X。

相关链接: http://www.cloudera.com/content/cloudera/en/products-and-services/cdh/impala.html

31. InfoBright社区版

InfoBright为数据分析而设计,这是一种面向列的数据库,具有很高的压缩比。InfoBright.com提供基于同一代码的收费产品,提供支持服务。

支持的操作系统:Windows和Linux。

相关链接: http://www.infobright.org

32. MongoDB

mongoDB的下载量已超过1000万人次,这是一种极其受欢迎的NoSQL数据库。MongoDB.com上提供了企业版、支持、培训及相关产品和服务。

支持的操作系统:Windows、Linux、OS X和Solaris。

相关链接: http://www.mongodb.org

33. Neo4j

Neo4j自称是“速度最快、扩展性最佳的原生图形数据库”,它承诺具有大规模扩展性、快速的密码查询性能和经过改进的开发效率。用户包括电子港湾、必能宝(Pitney Bowes)、沃尔玛、德国汉莎航空公司和CrunchBase。

支持的操作系统:Windows和Linux。

相关链接: http://neo4j.org

34. OrientDB

这款多模型数据库结合了图形数据库的一些功能和文档数据库的一些功能。提供收费支持、培训和咨询等服务。

支持的操作系统:与操作系统无关。

相关链接: http://www.orientdb.org/index.htm

35. Pivotal Greenplum Database

Pivotal声称,Greenplum是“同类中最佳的企业级分析数据库”,能够非常快速地对庞大的海量数据进行功能强大的分析。它是Pivotal大数据库套件的一部分。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://pivotal.io/big-data/pivotal-greenplum-database

36. Riak

Riak“功能完备”,有两个版本:KV是分布式NoSQL数据库,S2提供了面向云环境的对象存储。它既有开源版,也有商业版,还有支持Spark、Redis和Solr的附件。

支持的操作系统:Linux和OS X。

相关链接: http://basho.com/riak-0-10-is-full-of-great-stuff/

37. Redis

Redis现在由Pivotal赞助,这是一种键值缓存和存储系统。提供收费支持。要注意:虽然该项目并不正式支持Windows,不过微软在GitHub上有一个Windows派生版。

支持的操作系统:Linux。

相关链接: http://redis.io

四、商业智能

38. Talend Open Studio

Talend的下载量已超过200万人次,其开源软件提供了数据整合功能。该公司还开发收费的大数据、云、数据整合、应用程序整合和主数据管理等工具。其用户包括美国国际集团(AIG)、康卡斯特、电子港湾、通用电气、三星、Ticketmaster和韦里逊等企业组织。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.talend.com/index.php

39. Jaspersoft

Jaspersoft提供了灵活、可嵌入的商业智能工具,用户包括众多企业组织:高朋、冠群科技、美国农业部、爱立信、时代华纳有线电视、奥林匹克钢铁、内斯拉斯加大学和通用动力公司。除了开源社区版外,它还提供收费的报表版、亚马逊网络服务(AWS)版、专业版和企业版。

支持的操作系统:与操作系统无关。

相关链接: http://www.jaspersoft.com

40. Pentaho

Pentaho归日立数据系统公司所有,它提供了一系列数据整合和业务分析工具。官方网站上提供了三个社区版;访问Pentaho.com,即可了解收费支持版方面的信息。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://community.pentaho.com

41. SpagoBI

Spago被市场分析师们称为“开源领袖”,它提供商业智能、中间件和质量保证软件,另外还提供Java EE应用程序开发框架。该软件百分之分免费、开源,不过也提供收费的支持、咨询、培训及其他服务。

支持的操作系统:与操作系统无关。

相关链接: http://www.spagoworld.org/xwiki/bin/view/SpagoWorld/

42. KNIME

KNIME的全称是“康斯坦茨信息挖掘工具”(Konstanz Information Miner),这是一种开源分析和报表平台。提供了几个商业和开源扩展件,以增强其功能。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.knime.org

43. BIRT

BIRT的全称是“商业智能和报表工具”。它提供的一种平台可用于制作可以嵌入到应用程序和网站中的可视化元素及报表。它是Eclipse社区的一部分,得到了Actuate、IBM和Innovent Solutions的支持。

支持的操作系统:与操作系统无关。

相关链接: http://www.eclipse.org/birt/

五、数据挖掘

44.DataMelt

作为jHepWork的后续者,DataMelt可以处理数学运算、数据挖掘、统计分析和数据可视化等任务。它支持Java及相关的编程语言,包括Jython、Groovy、JRuby和Beanshell。

支持的操作系统:与操作系统无关。

相关链接: http://jwork.org/dmelt/

45. KEEL

KEEL的全称是“基于进化学习的知识提取”,这是一种基于Java的机器学习工具,为一系列大数据任务提供了算法。它还有助于评估算法在处理递归、分类、集群、模式挖掘及类似任务时的效果。

支持的操作系统:与操作系统无关。

相关链接: http://keel.es

46. Orange

Orange认为数据挖掘应该是“硕果累累、妙趣横生”,无论你是有多年的丰富经验,还是刚开始接触这个领域。它提供了可视化编程和Python脚本工具,可用于数据可视化和分析。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://orange.biolab.si

47. RapidMiner

RapidMiner声称拥有250000多个用户,包括贝宝、德勤、电子港湾、思科和大众。它提供一系列广泛的开源版和收费版,不过要注意:免费的开源版只支持CSV格式或Excel格式的数据。

支持的操作系统:与操作系统无关。

相关链接: https://rapidminer.com

48. Rattle

Rattle的全称是“易学易用的R分析工具”。它为R编程语言提供了一种图形化界面,简化了这些过程:构建数据的统计或可视化摘要、构建模型以及执行数据转换。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://rattle.togaware.com

49. SPMF

SPMF现在包括93种算法,可用于顺序模式挖掘、关联规则挖掘、项集挖掘、顺序规则挖掘和集群。它可以独立使用,也可以整合到其他基于Java的程序中。

支持的操作系统:与操作系统无关。

相关链接: http://www.philippe-fournier-viger.com/spmf/

50. Weka

怀卡托知识分析环境(Weka)是一组基于Java的机器学习算法,面向数据挖掘。它可以执行数据预处理、分类、递归、集群、关联规则和可视化。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.cs.waikato.ac.nz/~ml/weka/

六、查询引擎

51. Drill

这个Apache项目让用户可以使用基于SQL的查询,查询Hadoop、NoSQL数据库和云存储服务。它可用于数据挖掘和即席查询,它支持一系列广泛的数据库,包括HBase、MongoDB、MapR-DB、HDFS、MapR-FS、亚马逊S3、Azure Blob Storage、谷歌云存储和Swift。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://drill.apache.org

七、编程语言

52. R

R类似S语言和环境,旨在处理统计计算和图形。它包括一套整合的大数据工具,可用于数据处理、计算和可视化。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.r-project.org

53. ECL

企业控制语言(ECL)是开发人员用来在HPCC平台上构建大数据应用程序的语言。HPCC Systems官方网站上有集成开发环境(IDE)、教程以及处理该语言的众多相关工具。

支持的操作系统:Linux。

相关链接: http://hpccsystems.com/download/docs/ecl-language-reference

八、大数据搜索

54. Lucene

基于Java的Lucene可以非常迅速地执行全文搜索。据官方网站声称,它在现代硬件上每小时能够检索超过150GB的数据,它含有强大而高效的搜索算法。开发工作得到了Apache软件基金会的赞助。

支持的操作系统:与操作系统无关。

相关链接: http://lucene.apache.org/core/

55. Solr

Solr基于Apache Lucene,是一种高度可靠、高度扩展的企业搜索平台。知名用户包括eHarmony、西尔斯、StubHub、Zappos、百思买、AT&T、Instagram、Netflix、彭博社和Travelocity。

支持的操作系统:与操作系统无关。

相关链接: http://lucene.apache.org/solr/

九、内存中技术

56. Ignite

这个Apache项目自称是“一种高性能、整合式、分布式的内存中平台,可用于对大规模数据集执行实时计算和处理,速度比传统的基于磁盘的技术或闪存技术高出好几个数量级。”该平台包括数据网格、计算网格、服务网格、流媒体、Hadoop加速、高级集群、文件系统、消息传递、事件和数据结构等功能。

支持的操作系统:与操作系统无关。

相关链接: https://ignite.incubator.apache.org

57. Terracotta

Terracotta声称其BigMemory技术是“世界上数一数二的内存中数据管理平台”,声称拥有210万开发人员,250家企业组织部署了其软件。该公司还提供商业版软件,另外提供支持、咨询和培训等服务。

支持的操作系统:与操作系统无关。

相关链接: http://www.terracotta.org

58. Pivotal GemFire/Geode

今年早些时候,Pivotal宣布它将开放其大数据套件关键组件的源代码,其中包括GemFire内存中NoSQL数据库。它已向Apache软件基金会递交了一项提案,以便在“Geode”的名下管理GemFire数据库的核心引擎。还提供该软件的商业版。

支持的操作系统:Windows和Linux。

相关链接: http://pivotal.io/big-data/pivotal-gemfire

59. GridGain

由Apache Ignite驱动的GridGrain提供内存中数据结构,用于迅速处理大数据,还提供基于同一技术的Hadoop加速器。它既有收费的企业版,也有免费的社区版,后者包括免费的基本支持。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.gridgain.com

60. Infinispan

作为一个红帽JBoss项目,基于Java的Infinispan是一种分布式内存中数据网格。它可以用作缓存、用作高性能NoSQL数据库,或者为诸多框架添加集群功能。

支持的操作系统:与操作系统无关。

相关链接: http://www.jboss.org/infinispan.html

转载自:   http://os.51cto.com/art/201508/487936_all.htm 译者: 布加迪

更多大数据与分析相关行业资讯、解决方案、案例、教程等请点击查看>>>

详情请咨询在线客服

客服热线:023-66090381

基于Redis的限流系统的设计

本文讲述基于Redis的限流系统的设计,主要会谈及限流系统中限流策略这个功能的设计;在实现方面,算法使用的是令牌桶算法来,访问Redis使用lua脚本。

1、概念

In computer networks, rate limiting is used to control the rate of traffic sent or received by a network interface controller and is used to prevent DoS attacks

用我的理解翻译一下:限流是对系统的出入流量进行控制,防止大流量出入,导致资源不足,系统不稳定。

限流系统是对资源访问的控制组件,控制主要的两个功能:限流策略熔断策略,对于熔断策略,不同的系统有不同的熔断策略诉求,有的系统希望直接拒绝、有的系统希望排队等待、有的系统希望服务降级、有的系统会定制自己的熔断策略,很难一一列举,所以本文只针对限流策略这个功能做详细的设计。

针对限流策略这个功能,限流系统中有两个基础概念:资源和策略。

  • 资源 :或者叫稀缺资源,被流量控制的对象;比如写接口、外部商户接口、大流量下的读接口
  • 策略 :限流策略由限流算法和可调节的参数两部分组成

熔断策略:超出速率阈值的请求处理策略,是我自己理解的一个叫法,不是业界主流的说法。

2、限流算法

  • 限制瞬时并发数
  • 限制时间窗最大请求数
  • 令牌桶

2.1、限制瞬时并发数

定义:瞬时并发数,系统同时处理的请求/事务数量

优点:这个算法能够实现控制并发数的效果

缺点:使用场景比较单一,一般用来对入流量进行控制

java伪代码实现

AtomicInteger atomic = new AtomicInteger(1)
try {    
    if(atomic.incrementAndGet() > 限流数) {   
        //熔断逻辑
    } else {
        //处理逻辑
    } 
} finally {
    atomic.decrementAndGet();
}

2.2、限制时间窗最大请求数

定义:时间窗最大请求数,指定的时间范围内允许的最大请求数

优点:这个算法能够满足绝大多数的流控需求,通过时间窗最大请求数可以直接换算出最大的QPS(QPS = 请求数/时间窗)

缺点:这种方式可能会出现流量不平滑的情况,时间窗内一小段流量占比特别大

lua代码实现

--- 资源唯一标识
local key = KEYS[1]
--- 时间窗最大并发数
local max_window_concurrency = tonumber(ARGV[1])  
--- 时间窗
local window = tonumber(ARGV[2])   
--- 时间窗内当前并发数
local curr_window_concurrency = tonumber(redis.call('get', key) or 0)  
if current + 1 > limit then
    return false
else
    redis.call("INCRBY", key,1)    
    if window > -1 then
        redis.call("expire", key,window)    
    end
    return true
end

2.3、令牌桶

算法描述

  • 假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中
  • 假设桶中最多可以存放b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃
  • 当流量以速率v进入,从桶中以速率v取令牌,拿到令牌的流量通过,拿不到令牌流量不通过,执行熔断逻辑

属性

  • 长期来看,符合流量的速率是受到令牌添加速率的影响,被稳定为:r
  • 因为令牌桶有一定的存储量,可以抵挡一定的流量突发情况
    • M是以字节/秒为单位的最大可能传输速率:M>r
    • T max = b/(M-r)    承受最大传输速率的时间
    • B max = T max * M   承受最大传输速率的时间内传输的流量

优点:流量比较平滑,并且可以抵挡一定的流量突发情况

因为我们限流系统的实现就是基于令牌桶这个算法,具体的代码实现参考下文。

3、工程实现

3.1、技术选型

  • mysql:存储限流策略的参数等元数据
  • redis+lua:令牌桶算法实现

说明:因为我们把redis 定位为:缓存、计算媒介,所以元数据都是存在db中

3.2、架构图

3.3、 数据结构

字段 描述
name 令牌桶的唯一标示
apps 能够使用令牌桶的应用列表
max_permits 令牌桶的最大令牌数
rate 向令牌桶中添加令牌的速率
created_by 创建人
updated_by 更新人

限流系统的实现是基于redis的,本可以和应用无关,但是为了做限流元数据配置的统一管理,按应用维度管理和使用,在数据结构中加入了apps这个字段,出现问题,排查起来也比较方便。

3.4、代码实现

3.4.1、代码实现遇到的问题

参考令牌桶的算法描述,一般思路是在RateLimiter-client放一个重复执行的线程,线程根据配置往令牌桶里添加令牌,这样的实现由如下缺点:

  • 需要为每个令牌桶配置添加一个重复执行的线程
  • 重复的间隔精度不够精确:线程需要每1/r秒向桶里添加一个令牌,当r>1000 时间线程执行的时间间隔根本没办法设置(从后面性能测试的变现来看RateLimiter-client 是可以承担 QPS > 5000 的请求速率)

3.4.2、解决方案

基于上面的缺点,参考了google的guava中RateLimiter中的实现,我们使用了触发式添加令牌的方式。

算法描述

  • 基于上述的令牌桶算法
  • 将添加令牌改成触发式的方式,取令牌的是做添加令牌的动作
  • 在去令牌的时候,通过计算上一次添加令牌和当前的时间差,计算出这段间应该添加的令牌数,然后往桶里添加
    • curr_mill_second = 当前毫秒数
    • last_mill_second = 上一次添加令牌的毫秒数
    • r = 添加令牌的速率
    • reserve_permits = (curr_mill_second-last_mill_second)/1000 * r
  • 添加完令牌之后再执行取令牌逻辑

3.4.3、 lua代码实现

--- 获取令牌
--- 返回码
--- 0 没有令牌桶配置
--- -1 表示取令牌失败,也就是桶里没有令牌
--- 1 表示取令牌成功
--- @param key 令牌(资源)的唯一标识
--- @param permits  请求令牌数量
--- @param curr_mill_second 当前毫秒数
--- @param context 使用令牌的应用标识
local function acquire(key, permits, curr_mill_second, context)
    local rate_limit_info = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate", "apps")    
    local last_mill_second = rate_limit_info[1]    
    local curr_permits = tonumber(rate_limit_info[2])    
    local max_permits = tonumber(rate_limit_info[3])    
    local rate = rate_limit_info[4]    
    local apps = rate_limit_info[5]    
    --- 标识没有配置令牌桶
    if type(apps) == 'boolean' or apps == nil or not contains(apps, context) then
        return 0
    end
    local local_curr_permits = max_permits;    
    --- 令牌桶刚刚创建,上一次获取令牌的毫秒数为空
    --- 根据和上一次向桶里添加令牌的时间和当前时间差,触发式往桶里添加令牌
    --- 并且更新上一次向桶里添加令牌的时间
    --- 如果向桶里添加的令牌数不足一个,则不更新上一次向桶里添加令牌的时间
    if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= false and last_mill_second ~= nil) then
        local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate)        
        local expect_curr_permits = reverse_permits + curr_permits;
        local_curr_permits = math.min(expect_curr_permits, max_permits);       
         --- 大于0表示不是第一次获取令牌,也没有向桶里添加令牌
        if (reverse_permits > 0) then
            redis.pcall("HSET", key, "last_mill_second", curr_mill_second)       
      end
    else
        redis.pcall("HSET", key, "last_mill_second", curr_mill_second)   
    end
    local result = -1
    if (local_curr_permits - permits >= 0) then
        result = 1
        redis.pcall("HSET", key, "curr_permits", local_curr_permits - permits)    
    else
        redis.pcall("HSET", key, "curr_permits", local_curr_permits)    
    end
    return result
end

关于限流系统的所有实现细节,我都已经放到github上,gitbub地址:https://github.com/wukq/rate-limiter,有兴趣的同学可以前往查看,由于笔者经验与知识有限,代码中如有错误或偏颇,欢迎探讨和指正。

3.4.4、管理界面

前面的设计中,限流的配置是和应用关联的,为了更够更好的管理配置,需要一个统一的管理页面去对配置进行管控:

  • 按应用对限流配置进行管理
  • 不同的人分配不同的权限;相关人员有查看配置的权限,负责人有修改和删除配置的权限

3.5、性能测试

配置:aws-elasticcache-redis 2核4g

因为Ratelimiter-client的功能比较简单,基本上是redis的性能打个折扣。

  • 单线程取令牌:Ratelimiter-client的 QPS = 250/s
  • 10个线程取令牌:Ratelimiter-client的 QPS = 2000/s
  • 100个线程取令牌:Ratelimiter-client的 QPS = 5000/s

4、总结

限流系统从设计到实现都比较简单,但是确实很实用,用四个字来形容就是:短小强悍,其中比较重要的是结合公司的权限体系和系统结构,设计出符合自己公司规范的限流系统。

不足

  • redis 我们用的是单点redis,只做了主从,没有使用redis高可用集群(可能使用redis高可用集群,会带来新的问题)
  • 限流系统目前只做了应用层面的实现,没有做接口网关上的实现
  • 熔断策略需要自己定制,如果实现的好一点,可以给一些常用的熔断策略模板

参考书籍:

1.《Redis 设计与实现》
2.《Lua编程指南》

参考文章:

1. redis官网

2. lua编码规范

3. 聊聊高并发系统之限流特技

4. guava Ratelimiter 实现

5. Token_bucket wiki 词条