sql server 并发

介绍 (Introduction)

Intended audience

目标听众

This document is intended for application developers and database administrators who are willing to get an overview of comm concurrency problems to which transaction isolation levels respond in the particular case of Microsoft SQL Server.

本文档适用于愿意概述通信并发问题的应用程序开发人员和数据库管理员,在Microsoft SQL Server的特殊情况下,事务隔离级别会对此问题做出响应。

Typographical Conventions

印刷约定

Convention Meaning
Stylized Consolas Font
Used for blocks of code, commands and script examples. Text should be interpreted exactly as presented.
Consolas Font Used for inline code, commands or examples. Text should be interpreted exactly as presented.
<italic font in brackets> Italic texts set in angle brackets denote a variable requiring substitution for a real value.
Italic font Used to denote the title of a book, article, or other publication.
Note Additional information or caveats.
About screen capture and images
  • Selections and arrows default color is dark red (Red 192, Green 0 and Blue 0)
  • Selections and arrows width is limited to 2 px
  • Selections are rectangles
  • Arrows are used to show relations between important parts of the screen capture
  • Screen captures won’t comprise user desktop or unnecessary white space
  • Highlights on text in an image should be done using yellow color with straight edges.
  • Confidential information should be properly cut off, not blurred. You can simply cut the part of or color it so that it matches the background.
惯例 含义
用于代码块,命令和脚本示例。 文字的解释应与提出的完全相同。
Consolas字体 用于内联代码,命令或示例。 文字的解释应与提出的完全相同。
<括号中的斜体> 尖括号中设置的斜体文本表示需要替换实数值的变量。
斜体字体 用于表示书籍,文章或其他出版物的标题。
注意 其他信息或警告。
关于屏幕截图和图像
  • 选项和箭头的默认颜色为深红色(红色192,绿色0和蓝色0)
  • 选择和箭头宽度限制为2像素
  • 选择是矩形
  • 箭头用于显示屏幕截图重要部分之间的关​​系
  • 屏幕截图不会包含用户桌面或不必要的空白
  • 图像中文本的高亮应使用带有直边的黄色完成。
  • 机密信息应适当切断,而不是模糊不清。 您可以简单地切割或对其进行着色,使其与背景匹配。

Context

语境

In any relational database system, there is the concept of transaction. A transaction is a set of logical operations that have to be performed in a user session as a single piece of work. Let’s review the properties of a transaction.

在任何关系数据库系统中,都存在事务的概念。 事务是一组必须在用户会话中作为单个工作执行的逻辑操作。 让我们回顾一下事务的属性。

Hence, a transaction must be atomic i.e. there is no halfway for it to complete: either all the logical operations occur or none of them occur.

因此,事务必须是原子的,即没有半路完成:要么发生所有逻辑操作,要么都不发生。

A transaction has to be consistent i.e. any data written using database system must be valid according to defined rules like primary key uniqueness or foreign key constraints.

事务必须是一致的,即,根据定义的规则(例如主键唯一性或外键约束),使用数据库系统写入的任何数据都必须有效。

Once the consistency is ensured, a transaction must be permanently stored to disk. It guarantees the durability required for a transaction.

一旦确保了一致性,就必须将事务永久存储到磁盘。 它保证了交易所需的持久性。

Last but not least, as multiple transactions could be running concurrently in a database system, we can find transactions that read from or writes to the same data object (row, table, index…). It introduces a set of problems to which different « transaction isolation (level) » tend to respond. A transaction isolation (level) defines how and when the database system will present changes made by any transaction to other user sessions.

最后但并非最不重要的一点是,由于一个数据库系统中可以同时运行多个事务,因此我们可以找到读取或写入同一数据对象(行,表,索引等)的事务。 它引入了一系列问题,不同的“事务隔离(级别)”倾向于响应这些问题。 事务隔离(级别)定义了数据库系统如何以及何时将任何事务所做的更改呈现给其他用户会话。

In order to select the appropriate transaction isolation level, having a good understanding on common concurrency problems that can occur is mandatory.

为了选择适当的事务隔离级别,必须对可能发生的常见并发问题有一个很好的了解。

This article is the first one of a series about transaction isolation level. It is divided into two parts. The first one will explain the concurrency problems with a theoretical example while the second will be more practical and we will try to experience these problems on a SQL Server instance.

本文是有关事务隔离级别的系列文章中的第一篇。 它分为两个部分。 第一个将通过理论示例解释并发问题,而第二个将更实际,我们将尝试在SQL Server实例上体验这些问题。

并发问题 (Concurrency problems)

Before diving into transaction levels details, it’s important to get used to typical concurrency problems and how we call them.

在深入研究交易级别细节之前,重要的是要习惯于典型的并发问题以及我们如何称呼它们。

Lost update and dirty write

更新丢失,写脏

This phenomenon happens when two transactions access the same record and both updates this record. The following figure summarizes what could happen in a simple example.

当两个事务访问相同的记录并且都更新该记录时,就会发生这种现象。 下图总结了一个简单示例中可能发生的情况。

In this example, we have 2 concurrent transactions that access a record with a (60) modifiable value. This record is identified either by its rowId or by a primary key column that won’t be presented here for simplicity.

在此示例中,我们有2个并发事务访问具有(60)可修改值的记录。 该记录由其rowId或主键列标识,为简单起见,此处将不显示该记录。

The first transaction reads this record, does some processing then updates this record and finally commits its work. The second transaction reads the record then updates it immediately and commits. Both transactions do not update this record to the same value. This leads to a loss for the update statement performed by second transaction.

第一个事务读取该记录,进行一些处理,然后更新该记录,最后提交其工作。 第二个事务读取记录,然后立即更新并提交。 这两个事务都不会将此记录更新为相同的值。 这导致第二笔交易执行的更新语句丢失。

As Transaction 1 overwrites a value that Transaction 2 already modified. We could have said that Transaction 1 did a « dirty write » if Transaction 2 didn’t commit its work.

事务1覆盖事务2已经修改的值时。 我们可以说,如果事务2不提交其工作,则事务1会执行“脏写”操作。

Dirty read

脏读

A dirty read happens when a transaction accesses a data that has been modified by another transaction but this change has not been committed or rolled back yet. Following figure shows a case of dirty read. In this example, record has two columns with a starting value of (60,40). In this context, let’s say we have an applicative constraint that says that the sum of those values must always be 100.

当事务访问已被另一事务修改的数据但尚未提交或回滚此更改时,将发生脏读。 下图显示了脏读的情况。 在此示例中,记录有两列,起始值为(60,40)。 在这种情况下,假设我们有一个应用约束,该约束说这些值的总和必须始终为100。

Non-repeatable read or fuzzy read

不可重复读取或模糊读取

The situation of non-repeatable read is almost the same as dirty read except that both values are modified. As in previous sub-sections, we will review a graphical representation of a non-repeatable read situation. To do so, we will keep two concurrent transactions accessing two columns of the same record. One of them reads and modifies each value, one at a time, then commits while the other reads the first value, does some processing, reads the second value then commits. Keeping our constraint from previous example (the sum of both values equals 100), the presented situation leads the second transaction to manipulate inconsistent data and maybe to present it to an end-user.

不可重复读取的情况与脏读取几乎相同,不同之处在于两个值均被修改。 与前面的小节一样,我们将回顾不可重复读取情况的图形表示。 为此,我们将保持两个并发事务访问同一记录的两列。 其中一个读取和修改每个值,一次一次,然后提交,而另一个读取第一个值,进行一些处理,读取第二个值,然后提交。 保持前面示例的约束(两个值的总和等于100),所呈现的情况导致第二次事务处理不一致的数据,并可能将其呈现给最终用户。

Phantom reads

幻影阅读

Phantom reads are a variation of non-repeatable reads in the context of row sets. Here is an example that illustrates this:

幻像读取是行集上下文中不可重复读取的一种变体。 这是说明此的示例:

Let’s say we have a transaction Transaction 1 that performs twice a SELECT query against a table T, once at its beginning and once just before its end. Let’s assume another transaction Transaction 2 starts after the first one, inserts a new row to the table T and commits before the second time Transaction 1 will run its SELECT query. The result sets that will be returned by the two occurrences of the SELECT query will differ.

假设我们有一个事务Transaction 1 ,它对表T执行两次SELECT查询,一次在表T的开始,一次在表T的结束。 假设另一个事务Transaction 2在第一个事务之后开始,在表T中插入新行,并在第二次事务1运行其SELECT查询之前提交。 两次SELECT查询返回的结果集将有所不同。

Here is a diagram that summarizes the situation:

这是一个汇总情况的图表:

Locking reads

锁定读取

This is not really a concurrency problem, but more likely a “design pattern”. In short, the principle is to read a value from a given record and update this record based on the returned value inside the same transaction, with the insurance that no other session will modify the value that has just been read.

这并不是真正的并发问题,而更可能是“设计模式”。 简而言之,原理是从给定记录中读取一个值,并根据同一笔交易中返回的值来更新该记录,以确保没有其他会话会修改刚刚读取的值。

It’s the concept of SELECT FOR UPDATE in Oracle or SELECT … FROM <table> WITH (UPDLOCK) in SQL Server.

它是Oracle中的SELECT FOR UPDATE或SQL Server中的SELECT…FROM <table> WITH(UPDLOCK)的概念

This will only work in SERIALIZABLE isolation level. We won’t discuss it anymore in this article.

这仅适用于SERIALIZABLE隔离级别。 在本文中我们将不再讨论。

在SQL Server上进行实验 (Experimentation on SQL Server)

The experimentation scripts presented here are all designed using the AdventureWorks2012 database.

此处介绍的实验脚本都是使用AdventureWorks2012数据库设计的。

If no precision is made about a transaction isolation level in the experimentation detailed explanation, the results presented are those returned using SQL Server’s default transaction isolation level, which is READ COMMITTED.

如果在实验详细说明中未对事务隔离级别进行精确说明,则显示的结果是使用SQL Server的默认事务隔离级别READ COMMITTED返回的结果。

Lost update

更新失败

Following queries depict the following scenario.

以下查询描述了以下情况。

We are in a bank system. Your bank account has an initial balance of 1500 (currency does not matter). You will find below the T-SQL statement to set up the test.

我们在银行系统中。 您的银行帐户的初始余额为1500(货币无关紧要)。 您将在T-SQL语句下面找到设置测试。

CREATE TABLE BankAccounts(AccountId      INT IDENTITY(1,1),BalanceAmount   INT
);insert into BankAccounts (BalanceAmount
)
SELECT 1500
;

Your employer attempts to pay your (tiny) salary, let’s say 1600. This will consist of your first session for which you will find T-SQL statement corresponding to the action that has to be done inside the bank system.

您的雇主尝试支付您的(微不足道的)薪水,比方说1600。这将包括您的第一次会话,在该会话中,您将找到与银行系统内必须执行的操作相对应的T-SQL语句。

-- Session 1: Employer
DECLARE @CustomerBalance   INT ;
DECLARE @BalanceDifference INT ;SET @BalanceDifference = 1600 ;BEGIN TRANSACTION ;-- Getting back current balance valueSELECT @CustomerBalance = BalanceAmountFROM BankAccountsWHERE AccountId = 1 ;PRINT 'Read Balance value: ' + CONVERT(VARCHAR(32),@CustomerBalance);-- adding salary amountSET @CustomerBalance = @CustomerBalance + @BalanceDifference ;-- Slowing down transaction to let tester the time-- to run query for other sessionPRINT 'New Balance value: ' + CONVERT(VARCHAR(32),@CustomerBalance);WAITFOR DELAY '00:00:10.000';-- updating in tableUPDATE BankAccountsSET BalanceAmount = @CustomerBalance WHERE AccountId = 1 ;-- display results for userSELECT BalanceAmount as BalanceAmountSession1FROM BankAccountsWHERE AccountId = 1 ;
COMMIT ;

At the same time, as you’ve returned an article to your favorite web reseller, he’s also trying to add 40 to your bank account. Following code will be run:

同时,当您将文章退还给您最喜欢的网络经销商时,他还试图将40加到您的银行帐户中。 将运行以下代码:

-- Session 2: Web resellerDECLARE @CustomerBalance    INT ;
DECLARE @BalanceDifference INT ;SET @BalanceDifference = 40 ;BEGIN TRANSACTION ;-- Getting back current balance valueSELECT @CustomerBalance = BalanceAmountFROM BankAccountsWHERE AccountId = 1 ;PRINT 'Read Balance value: ' + CONVERT(VARCHAR(32),@CustomerBalance);-- adding salary amountSET @CustomerBalance = @CustomerBalance + @BalanceDifference ;PRINT 'New Balance value: ' + CONVERT(VARCHAR(32),@CustomerBalance);-- updating in tableUPDATE BankAccountsSET BalanceAmount = @CustomerBalance WHERE AccountId = 1 ;-- display results for userSELECT BalanceAmount as BalanceAmountSession2FROM BankAccountsWHERE AccountId = 1 ;
COMMIT ;

Here are the results we will get from final SELECT in each query:

这是我们将从每个查询中的最终SELECT获得的结果:

Unfortunately, we lost the money from web reseller…

不幸的是,我们从网络经销商那里亏损了……

Now, let’s clean up our test.

现在,让我们清理一下测试。

DROP TABLE BankAccounts ;

Dirty read

脏读

To illustrate dirty reads, we will update data in Person.Person table: we will update all records so that all rows where FirstName column value is “Aaron” will bear the same value for it’s their corresponding LastName column. This value will be “Hotchner” but won’t persist: we will rollback the transaction.

为了说明脏读,我们将更新Person.Person表中的数据:我们将更新所有记录,以便FirstName列值为“ Aaron”的所有行都将具有相同的值,因为它们是其对应的LastName列。 该值将为“ Hotchner”,但不会持久:我们将回滚事务。

Here is the script for the first session:

这是第一个会话的脚本:

SELECT COUNT(DISTINCT LastName) DistinctLastNameBeforeBeginTran
FROM Person.Person
WHERE FirstName = 'Aaron';BEGIN TRANSACTION;UPDATE Person.Person
SET LastName = 'Hotchner'
WHERE FirstName = 'Aaron'
;SELECT COUNT(DISTINCT LastName) DistinctLastNameInTransaction
FROM Person.Person
WHERE FirstName = 'Aaron';WAITFOR DELAY '00:00:10.000';ROLLBACK TRANSACTION;SELECT COUNT(DISTINCT LastName) DistinctLastNameAfterRollback
FROM Person.Person
WHERE FirstName = 'Aaron';

While the first session is in running its WAITFOR DELAY instruction, we can run following query in a second session:

当第一个会话正在运行其WAITFOR DELAY指令时,我们可以在第二个会话中运行以下查询:

SELECT COUNT(DISTINCT LastName) SecondSessionResults
FROM Person.Person
WHERE FirstName = 'Aaron';

With SQL Server’s default isolation level, the second session will be waiting for the first session to complete :

使用SQL Server的默认隔离级别,第二个会话将等待第一个会话完成:

So, we can say that, by default, SQL Server protects you from dirty reads. Let’s just change session isolation level to READ UNCOMMITTED and check that SQL Server will present « dirty » data to session 2 in that mode…

因此,我们可以说默认情况下,SQL Server保护您免受脏读的侵害。 让我们仅将会话隔离级别更改为READ UNCOMMITTED,并检查SQL Server将以该模式向会话2提供“脏”数据。

In order to change session’s transaction isolation level, run following query:

为了更改会话的事务隔离级别,请运行以下查询:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

Then, run the code for second session from above again.

然后,再次从上方运行第二个会话的代码。

Here are the results for both sessions:

这是两个会话的结果:

Non-repeatable reads

不可重复读

If you remind the explanation above about non-repeatable reads, this problem occurs when two consecutive reads of a given column value in a particular table row lead to two different values, meaning that values returned by a query are time-dependent even inside the same transaction.

如果您回想起上面关于不可重复读取的解释,则当特定表行中给定列值的两次连续读取导致两个不同的值时,就会发生此问题,这意味着即使同一查询内部的查询返回的值也是时间相关的交易。

Once again, we will use two sessions for this experiment. The first session will run a query returning five first rows from Person. Person table, wait for some time then rerun the exact same query.

再次,我们将使用两个会话进行此实验。 第一个会话将运行查询,以从Person返回前五行。 人员表,等待一段时间,然后重新运行完全相同的查询。

Here is the code for the first session:

这是第一次会话的代码:

-- ensure we use SQL Server default isolation level
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;BEGIN TRANSACTION;-- Query 1 - first run
SELECT TOP 5FirstName,MiddleName,LastName,Suffix
FROM Person.Person
ORDER BY LastName
;-- let some time for session 2
WAITFOR DELAY '00:00:10.000';-- Query 1 - second run
SELECT TOP 5FirstName,MiddleName,LastName,Suffix
FROM Person.Person
ORDER BY LastName
;COMMIT TRANSACTION;

While the first session is waiting, run following code that will update all records that have FirstName column value set to “Kim” and LastName column value set to “Abercrombie”.

在第一个会话等待时,运行以下代码,将更新FirstName列值设置为“ Kim”和LastName列值设置为“ Abercrombie”的所有记录。

-- ensure we use SQL Server default isolation level
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;BEGIN TRANSACTION;UPDATE Person.Person
SET Suffix = 'Clothes'
WHERE LastName = 'Abercrombie'
AND FirstName = 'Kim';COMMIT TRANSACTION;

Here are the result sets returned in the first session:

以下是在第一次会话中返回的结果集:

We can revert our changes with following query:

我们可以使用以下查询还原更改:

UPDATE Person.Person
SET Suffix = NULL
WHERE LastName = 'Abercrombie'
AND FirstName = 'Kim';

If we are willing to prevent this behavior to happen, we could have heard about the REPEATABLE READ isolation level. Let’s try it out!

如果我们愿意防止这种行为发生,我们可能已经听说过REPEATABLE READ隔离级别。 让我们尝试一下!

For the first session, we just need to change the first command from:

对于第一个会话,我们只需要将第一个命令从以下位置更改:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

to

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

and execute it. The code for second session can be used as is.

并执行它。 可以按原样使用第二个会话的代码。

With this simple change, we have a lock that is held by first session. This leads the second session to wait for the first one to complete before actually modify rows.

通过此简单的更改,我们在第一次会话中拥有了一个锁。 这导致第二个会话在实际修改行之前等待第一个会话完成。

Here is a screenshot that proves it:

这是证明它的屏幕截图:

And, obviously, we do not forget to revert our changes to the query shown above.

而且,显然,我们不会忘记将更改还原到上面显示的查询。

Phantom reads

幻影阅读

For this experiment, we will create a table called dbo.Employee and let it empty:

在本实验中,我们将创建一个名为dbo.Employee的表并将其为空:

IF (OBJECT_ID('dbo.Employee') IS NOT NULL)
BEGINDROP TABLE [dbo].[Employee];
END;CREATE TABLE [dbo].[Employee] (EmpId       int IDENTITY(1,1) NOT NULL,EmpName     nvarchar(32)      NOT NULL,CONSTRAINT pk_EmpId PRIMARY KEY CLUSTERED (EmpId)
);

In first session, we will run a SELECT query against this tablespace in time by 10 seconds:

在第一个会话中,我们将在10秒内针对此表空间运行SELECT查询:

-- ensure we use SQL Server default isolation level
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;BEGIN TRANSACTION;-- Query 1 - first run
SELECT *
FROM dbo.Employee
;-- let some time for session 2
WAITFOR DELAY '00:00:10.000';-- Query 1 - second run
SELECT *
FROM dbo.Employee
;COMMIT TRANSACTION;

In second session, we will insert some rows in dbo.Employee table.

在第二个会话中,我们将在dbo.Employee表中插入一些行。

-- ensure we use SQL Server default isolation level
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;BEGIN TRANSACTION;INSERT INTO [dbo].[Employee] ([EmpName]) VALUES ('Oby');
INSERT INTO [dbo].[Employee] ([EmpName]) VALUES ('One');
INSERT INTO [dbo].[Employee] ([EmpName]) VALUES ('Ken');
INSERT INTO [dbo].[Employee] ([EmpName]) VALUES ('Tukee');COMMIT TRANSACTION;

And here are the results sets returned in first session:

以下是在第一次会话中返回的结果集:

As we can see, duet o concurrency (and transaction isolation level), it’s possible to get different results sets for the same query, from time to time even in the same transaction.

如我们所见,由于并发(和事务隔离级别)的双重影响,即使在同一事务中,也可能会不时为同一查询获得不同的结果集。

摘要 (Summary)

So far, we’ve seen that the concurrency of transactions implies some basic issues that transaction isolation level can eventually prevent them to happen.

到目前为止,我们已经看到事务并发意味着一些基本问题,而事务隔离级别最终可以阻止它们发生。

In the next article, you will get more details about each transaction isolation level, included what common concurrency problem(s) they solve…

在下一篇文章中,您将获得有关每个事务隔离级别的更多详细信息,包括它们解决的常见并发问题。

参考资料 (References)

  • A Critique of ANSI SQL Isolation Levels ANSI SQL隔离级别的批判
  • Isolation Levels in the Database Engine 数据库引擎中的隔离级别
  • Difference between Non- repeatable read and Phantom read 不可重复读和幻像读之间的区别

翻译自: https://www.sqlshack.com/concurrency-problems-theory-and-experimentation-in-sql-server/

sql server 并发

sql server 并发_并发问题– SQL Server中的理论和实验相关推荐

  1. sql server死锁_如何解决SQL Server中的死锁

    sql server死锁 In this article, we will talk about the deadlocks in SQL Server, and then we will analy ...

  2. sql server 锁定_关于锁定SQL Server的全部

    sql server 锁定 .SQLCode { font-size: 13px; font-weight: bold; font-family: monospace;; white-space: p ...

  3. sql server死锁_如何报告SQL Server死锁事件

    sql server死锁 介绍 (Introduction) In the previous article entitled "What are SQL Server deadlocks ...

  4. sql server 群集_设计有效SQL Server群集索引

    sql server 群集 In the previous articles of this series (see bottom for a full index), we described, i ...

  5. sql server 数组_如何在SQL Server中实现类似数组的功能

    sql server 数组 介绍 (Introduction) I was training some Oracle DBAs in T-SQL and they asked me how to cr ...

  6. sql server 加密_列级SQL Server加密概述

    sql server 加密 This article gives an overview of column level SQL Server encryption using examples. 本 ...

  7. sql server 性能_如何在SQL Server中收集性能和系统信息

    sql server 性能 介绍 (Introduction) In this article, we're going through many of the tools we can use fo ...

  8. sql server 缓存_深入了解SQL Server缓冲区缓存

    sql server 缓存 When we talk about memory usage in SQL Server, we are often referring to the buffer ca ...

  9. sql dateadd函数_什么是SQL Server DATEADD()函数?

    sql dateadd函数 Hey, folks! In this article, we will be focusing on SQL Server DATEADD() function in d ...

最新文章

  1. 微信小程序 基础1【本页面窗口配置、组件、布局】
  2. 我在编写《微软System Center 2012 R2私有云部署实战》中应用的一些小技巧
  3. vue 多页面多模块分模块打包 分插件安装_Vue渲染方式
  4. Spring之IOC容器篇
  5. C#代码调用js函数,js函数中的document.getElementById(对象ID)得null值解决办法
  6. 服务器webpack构建性能,[译] 优化 WEBPACK 以更快地构建 REACT
  7. Cannot read lifecycle mapping metadata for artifact org.apache.maven.plugins问题的解决
  8. Shell中的Quoting
  9. 细说汽车电子通信总线之CAN-FD 总线协议详解
  10. 使用百度 EasyDL 实现电动车进电梯自动预警
  11. Docker配置阿里云镜像加速器以及镜像的常用操作命令
  12. typescript中this报错
  13. php 获取 星期几,php怎么获得星期几
  14. 赵小楼《天道》《遥远的救世主》深度解析(115)婚姻的观点
  15. 人工智能专家细数AI安全隐患
  16. 虚拟机安装报错-启动失败-Intel VT-x 处于禁用状态
  17. 我的世界自制mod{0}
  18. Java实现视频边加载边播放(利用http请求头的Range)
  19. 销售经理如何建立有效的客户档案?
  20. mplayer说明及常用命令

热门文章

  1. java获得map内存_[java]测试static的map的内存
  2. auc 和loss_精确率、召回率、F1 值、ROC、AUC 各自的优缺点是什么?
  3. python argv 详解_详解sys.argv[]的使用方法
  4. JPA学习笔记二——Hello World
  5. React Native按钮详解|Touchable系列组件使用详解
  6. 爬虫下载百度贴吧图片
  7. [nRF51822] 13、浅谈nRF51822和NRF24LE1/NRF24LU1/NRF24L01经典2.4G模块无线通信配置与流程...
  8. Smarty学习笔记(二)
  9. 《C++标准程序库》学习笔记(一)C++相关特性
  10. ASP.NET2.0 HiddenField控件