0

熟练Redis之无处不在的锁

 7 months ago
source link: https://blog.51cto.com/u_15696371/9587703
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
熟练Redis之无处不在的锁_Redis

为了保证并发访问的正确性,Redis提供了两种方法,分别是加锁和原子操作

Redis加锁两个问题:一个是,如果加锁操作多,会降低系统的并发访问性能;第二个是,Redis客户端要加锁时,需要用到分布式锁,而分布式锁实现复杂,需要用额外的存储系统来提供加解锁操作

一:无锁原子操作

原子操作是另一种提供并发访问控制的方法。原子操作是指执行过程保持原子性的操作,而且原子操作执行时并不需要再加锁,实现了无锁操作。这样一来,既能保证并发控制,还能减少对系统并发性能的影响。

1.并发访问中需要对什么进行控制?

1.1并发访问控制是什么

是指对多个客户端访问操作同一份数据的过程进行控制,以保证任何一个客户端发送的操作在Redis实例上执行时具有互斥性。例如,客户端A的访问操作在执行时,客户端B的操作不能执行,需要等到A的操作结束后,才能执行。

并发访问控制对应的操作主要是数据修改操作。当客户端需要修改数据时,基本流程分成两步:

1.客户端先把数据读取到本地,在本地进行修改;

2.客户端修改完数据后,再写回Redis。

这个流程叫做“读取-修改-写回”操作(Read-Modify-Write,简称为RMW操作)。当有多个客户端对同一份数据执行RMW操作的话,让RMW操作涉及的代码以原子性方式执行。访问同一份数据的RMW操作代码,就叫做临界区代码。当有多个客户端并发执行临界区代码时,就会存在一些潜在问题

1.2客户端更新商品库存的例子:

假设客户端要对商品库存执行扣减1的操作,伪代码如下所示:

current = GET(id)
current--
SET( id,current)

可以看到,客户端首先会根据商品id,从Redis中读取商品当前的库存值current(对应Read),然后,客户端对库存值减1(对应Modify),再把库存值写回Redis(对应Write)。当有多个客户端执行这段代码时,这就是一份临界区代码。

如果临界区代码的执行没有控制机制,就会出现数据更新错误。在刚才的例子中,假设现在有两个客户端A和B,同时执行刚才的临界区代码,就会出现错误:

熟练Redis之无处不在的锁_Redis_02

如果按正确的逻辑处理,客户端A和B对库存值各做了一次扣减,库存值应该为8。所以,这里的库存值明显更新错了。

出现这个现象的原因是,临界区代码中的客户端读取数据、更新数据、再写回数据涉及了三个操作,而这三个操作在执行时并不具有互斥性,多个客户端基于相同的初始值进行修改,而不是基于前一个客户端修改后的值再修改。

为了保证数据并发修改的正确性,可以用锁把并行操作变成串行操作,串行操作就具有互斥性。一个客户端持有锁后,其他客户端只能等到锁释放,才能拿锁再进行修改。

下面的伪代码显示了使用锁来控制临界区代码的执行情况,你可以看下。

LOCK()
current = GET(id)
current--

虽然加锁保证了互斥性,但是加锁也会导致系统并发性能降低。

如下图所示,当客户端A加锁执行操作时,客户端B、C就需要等待。A释放锁后,假设B拿到锁,那么C还需要继续等待,所以,t1时段内只有A能访问共享数据,t2时段内只有B能访问共享数据,系统的并发性能当然就下降了。

熟练Redis之无处不在的锁_客户端_03

和加锁类似,原子操作也能实现并发控制,但是原子操作对系统并发性能的影响较小.

2.Redis的两种原子操作方法

为了实现并发控制要求的临界区代码互斥执行,Redis的原子操作采用了两种方法:

  1. 把多个操作在Redis中实现成一个操作,也就是单命令操作;
  2. 把多个操作写到一个Lua脚本中,以原子性方式执行单个Lua脚本。

2.1Redis本身的单命令操作。

Redis是使用单线程来串行处理客户端的请求操作命令的,所以,当Redis执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的。当然,Redis的快照生成、AOF重写这些操作,可以使用后台线程或者是子进程执行,也就是和主线程的操作并行执行。不过,这些操作只是读取数据,不会修改数据,并不需要对它们做并发控制。

虽然Redis的单个命令操作可以原子性地执行,但是在实际应用中,数据修改时可能包含多个操作,至少包括读数据、数据增减、写回数据三个操作,这显然就不是单个命令操作了,那该怎么办呢?

进行增值/减值操作,而且它们本身就是单个命令操作,Redis在执行它们时,本身就具有互斥性。

比如说,在刚才的库存扣减例子中,客户端可以使用下面的代码,直接完成对商品id的库存值减1操作。即使有多个客户端执行下面的代码,也不用担心出现库存值扣减错误的问题。

decr id

执行的RMW操作是对数据进行增减值的话,Redis提供的原子操作INCR和DECR可以直接进行并发控制。
执行的操作不是简单地增减数据,那么,Redis的单命令操作已经无法保证多个操作的互斥执行了

2.2Lua脚本:

Redis会把整个Lua脚本作为一个整体执行。在执行的过程中不会被其他命令打断,从而保证了Lua脚本中操作的原子性。有多个换作要执行,无法用INCR/DECR这种命令操作来实现,可以把执行的操作编写到一个Lua脚本中,可以使用Redis的EVAL命令来执行脚本。这样一来,这些操作在执行时就具有了互斥性。

2.3Lua的使用例子:

当一个业务应用的访问用户增加时,有时需要限制某个客户端在一定时间范围内的访问次数,比如爆款商品的购买限流、社交网络中的每分钟点赞次数限制等。

那该怎么限制呢?

可以把客户端IP作为key,把客户端的访问次数作为value,保存到Redis中。客户端每访问一次后,我们就用INCR增加访问次数。

在这种场景下,客户端限流其实同时包含了对访问次数和时间范围的限制,例如每分钟的访问次数不能超过20。可以在客户端第一次访问时,给对应键值对设置过期时间,例如设置为60s后过期。

在客户端每次访问时,读取客户端当前的访问次数,如果次数超过阈值,就报错,限制客户端再次访问。

//获敏ip对应的访同次数
current = GET(ip)
//如果超过访问次数超过20次,则报错
IF current != NULL  AND current >20 THEN
		ERROR "exceed 20 accesses persecond"
ELSE
    //如果访问次数不定20次,增加一次访问计数
    value = INCR(ip)
    //如果是第一次访间,将键值对的过期时间设置为60s后
		IF value == 1  THEN
      EXPiRE(ip,60)
	  END
    //执行其他噪作
		DO THINGS

这个例子,使用了INCR来原子性地增加计数。但是,客户端限流的逻辑不只有计数,还包括访问次数判断和过期时间设置。对于这些操作,同样需要保证它们的原子性。

否则,如果客户端使用多线程访问,访问次数初始值为0,第一个线程执行了INCR(ip)操作后,第二个线程紧接着也执行了INCR(ip),此时, ip对应的访问次数就被增加到了2,我们就无法再对这个ip设置过期时间了。这样就会导致,这个ip对应的客户端访问次数达到20次之后,就无法再进行访问了。即使过了60s,也不能再继续访问,显然不符合业务要求。

所以,这个例子中的操作无法用Redis单个命令来实现,此时,我们就可以使用Lua脚本来保证并发控制。我们可以把访问次数加1、判断访问次数是否为1,以及设置过期时间这三个操作写入一个Lua脚本,如下所示:

local current

current = redis.call ( "incr", KEYS[1])
if tonumber (current) == 1 then
		redis.call( "expire" , KEYS[1],60)
end

假设编写的脚本名称为lua.script,接着就可以使用Redis客户端,带上eval选项,来执行该脚本。脚本所需的参数将通过以下命令中的keys和args进行传递。

redis-cli --eval lua.script keys , args

这样一来,访问次数加1、判断访问次数是否为1,以及设置过期时间这三个操作就可以原子性地执行了。即使客户端有多个线程同时执行这个脚本,Redis也会依次串行执行脚本代码,避免了并发操作带来的数据错误。

3.无锁原子操作小结

在并发访问时,并发的 RMW (read-modify-write)操作会导致数据错误,所以需要进行并发控制。所谓并发控制,就是要保证临界区代码的互斥执行。

Redis 提供了两种原子操作的方法来实现并发控制,分别是单命令操作和 Lua 脚本。因为原子操作本身不会对太多的资源限制访问,可以维持较高的系统并发性能。

但是,单命令原子操作的适用范围较小,并不是所有的 RMW 操作都能转变成单命令的原子操作(例如 INCR/DECR 命令只能在读取数据后做原子增减),需要对读取的数据做更多判断,或者对数据的修改不是简单的增减时,单命令操作就不适用。

Redis 的 Lua 脚本可以包含多个操作,这些操作都会以原子性的方式执行,绕开了单命令操作的限制。不过,如果把很多操作都放在 Lua 脚本中原子执行,会导致 Redis 执行脚本的时间增加,同样也会降低 Redis 的并发性能。

小建议:在编写 Lua 脚本时,要避免把不需要做并发控制的操作写入脚本中。

Redis在执行Lua脚本时,是可以保证原子性的,那么,在我举的Lua脚本例子(lua.script)中,你觉得是否需要把读取客户端ip的访问次数,也就是GET(ip),以及判断访问次数是否超过20的判断逻辑,也加到Lua脚本中吗?

答案:在这个例子中,要保证原子性的操作有三个,分别是INCR、判断访问次数是否为1和设置过期时间。而对于获取IP以及判断访问次数是否超过20这两个操作来说,它们只是读操作,即使客户端有多个线程并发执行这两个操作,也不会改变任何值,所以并不需要保证原子性,不用把它们放到Lua脚本中。

二:Redis中的分布式锁

Redis加锁可以应对并发问题,来控制并发写操作对共享数据的修改,从而保证数据的正确性

需要注意:

Redis属于分布式系统,当有多个客户端需要争抢锁时,必须要保证,这把锁不能是某个客户端本地的锁。否则的话,其它客户端是无法访问这把锁的,当然也就不能获取这把锁了

在分布式系统中,当有多个客户端需要获取锁时,需要分布式锁。此时,锁是保存在一个共享存储系统中的,可以被多个客户端共享访问和获取。

Redis本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁。而且Redis的读写性能高,可以应对高并发的锁操作场景。

日常在写程序的时候,经常会用到单机上的锁。而分布式锁和单机上的锁既有相似性,但也因为分布式锁是用在分

布式场景中,所以又具有一些特殊的要求。

1.单机上的锁和分布式锁的联系与区别

1.1单机上的锁

对于在单机上运行的多线程程序来说,锁本身可以用一个变量表示。

  • 变量值为0时,表示没有线程获取锁;
  • 变量值为1时,表示已经有线程获取到锁了。

1.1.1线程调用加锁和释放锁的操作说明

实际上,一个线程调用加锁操作,其实就是检查锁变量值是否为0。

如果是0,就把锁的变量值设置为1,表示获取到锁。

如果不是0,就返回错误信息,表示加锁失败,已经有别的线程获取到锁了。

一个线程调用释放锁操作,其实就是将锁变量的值置为0,以便其它线程可以来获取锁。

加锁和释放锁的操作,其中,lock为锁变量。

acquire__lock(){
  if lock ==0
     lock = 1
  	 return 1
	else
     return 0
}
release__lock(){
if lock = 0
  	return 1
}

1.1.2单机锁与分布式锁的相同点:

和单机上的锁类似,分布式锁也可以用一个变量来实现。客户端加锁和释放锁的操作逻辑,也和单机上的加锁和释放锁操作逻辑一致∶加锁时同祥需要判断锁变量的值,根据锁变量值来判断能否加锁成功;释放锁时需要把锁变量值设置为0,表明客户端不再持有锁。

1.1.3单机锁与分布式锁不同点:

和线程在单机上操作锁不同的是,在分布式场景下,锁变量需要由一个共享存储系统来维护,多个客户端才可以通过访问共享存储系统来访问锁变量。加锁和释放锁的操作就变成了读取、判断和设置共享存储系统中的锁变量值。

1.2实现分布式锁的两个要求。

要求一:分布式锁的加锁和释放锁的过程,涉及多个操作。所以,在实现分布式锁时,需要保证这些锁操作的原子性;

要求二∶共享存储系统保存了锁变量,如果共享存储系统发生故障或宕机,那么客户端也就无法进行锁操作了。在实现分布式锁时,需要考虑保证共享存储系统的可靠性,进而保证锁的可靠性。

既可以基于单个Redis节点来实现,也可以使用多个Redis节点实现。在这两种情况下,锁的可靠一性是不一样的

2.基于单个Redis节点实现分布式锁

作为分布式锁实现过程中的共享存储系统,Redis可以使用键值对来保存锁变量,再接收和处理不同客户端发送的加锁和释放锁的操作请求。

2.1键值对的键和值具体是确定方式

赋予锁变量一个变量名,作为键值对的键—》锁变量的值作为键值对的值—》Redis就能保存锁变量了,客户端也就可以通过Redis的命令操作来实现锁操作。

展示Redis使用键值对保存锁变量,以及两个客户端同时请求加锁的操作过程。

可以看到,Redis可以使用一个键值对lock_key:0来保存锁变量,其中,键是lock_key,也是锁变量的名称,锁变量的初始值是0。

2.2加锁操作:

在图中,客户端A和C同时请求加锁。因为Redis使用单线程处理请求,所以,即使客户端A和C同时把加锁请求发给了Redis,Redis也会串行处理它们的请求。

假设Redis先处理客户端A的请求,读取lock_key的值,发现lock_key为0,所以,Redis就把lock_key的value置为1,表示已经加锁了。紧接着,Redis处理客户端C的请求,此时,Redis会发现lock_key的值已经为1了,所以就返回加锁失败的信息。

2.3释放锁操作

释放锁就是直接把锁变量值设置为0。

下边这张图片展示了客户端A请求释放锁的过程。当客户端A持有锁时,锁变量lock_key的值为1。客户端A执行释放锁操作后,Redis将lock_key的值置为0,表明已经没有客户端持有锁了。

加锁包含了三个操作(读取锁变量、判断锁变量值以及把锁变量值设置为1),而这三个操作在执行时需要保证原子性。

2.4如何保证加锁数据原子性:

要想保证操作的原子性,有两种通用的方法,分别是使用Redis的单命令操作和使用Lua脚本。

原子性:原子性是指一个操作要么全部执行成功,要么完全不执行,没有中间状态。换句话说,原子性可以保证一系列操作是一个不可分割的整体,要么全部执行成功,要么全部失败,从而保证了数据的一致性和完整性。

在分布式加锁场景下,该怎么应用这两个方法呢?

2.4.1Redis可以用哪些单命令操作实现加锁操作:SETNX and del

首先是SETNX命令,它用于设置键值对的值。具体来说,就是这个命令在执行时会判断键值对是否存在,如果不存在,就设置键值对的值,如果存在,就不做任何设置。

举个例子,如果执行下面的命令时,key不存在,那么key会被创建,并且值会被设置为value;如果key已经存在,SETNX不做任何赋值操作。

SETNX key value

对于释放锁操作来说,可以在执行完业务逻辑后,使用DEL命令删除锁变量。不用担心锁变量被删除后,其他客户端无法请求加锁了。

因为SETNX命令在执行时,如果要设置的键值对(也就是锁变量)不存在,SETNX命令会先创建键值对,然后设置它的值。所以,释放锁之后,再有客户端请求加锁时,SETNX命令会创建保存锁变量的键值对,并设置锁变量的值,完成加锁。

总结来说,可以用SETNX和DEL命令组合来实现加锁和释放锁操作。下面的伪代码示例显示了锁操作

/加锁
SETNX lock_key 1
/业务逻辑
Do THINGS
//释放锁
DEL lock_key

2.4.2使用SETNX和DEL命令组合实现分布锁,存在两个潜在的风险。

第一个风险

假如某个客户端在执行了SETNX命令、加锁之后,紧接着却在操作共享数据时发生了异常,结果一直没有执行最后的DEL命令释放锁。因此,锁就一直被这个客户端持有,其它客户端无法拿到锁,也无法访问共享数据和执行后续操作,这会给业务应用带来影响。

一个有效的解决方法是,给锁变量设置一个过期时间。这样一来,即使持有锁的客户端发生了异常,无法主动地释放锁,Redis也会根据锁变量的过期时间,在锁变量过期后,把它删除。其它客户端在锁变量过期后,就可以重新请求加锁,,这就不会出现无法加锁的问题了。

第二个风险

如果客户端A执行了SETNX命令加锁后,假设客户端B执行了DEL命令释放锁,此时,客户端A的锁就被误释放了。如果客户端C正好也在申请加锁,就可以成功获得锁,进而开始操作共享数据。这样一来。客户端A和C同时在对共享数据进行操作,数据就会被修改错误,这也是业务层不能接受的。

需要能区分来自不同客户端的锁操作

可以在锁变量的值上处理:

在使用SETNX命令进行加锁的方法中,通过把锁变量值设置为1或0,表示是否加锁成功。1和0只有两种状态,无法表示究竟是哪个客户端进行的锁操作。在加锁操作时,可以让每个客户端给锁变量设置一个唯一值,这里的唯一值就可以用来标识当前操作的客户端。在释放锁操作时,客户端需要判断,当前锁变量的值是否和自己的唯一标识相等,只有在相等的情况下,才能释放锁,就不会出现误释放锁的问题。

在Redis中,具体是怎么实现的呢?

2.4.3单节点保证原子性的加锁操作

SETNX命令,对于不存在的键值对,它会先创建再设置值(也就是“不存在即设置”),为了能达到和SETNX命令一样的效果,Redis给SET命令提供了类似的选项NX,用来实现“不存在即设置”。如果使用了NX选项,SET命令只有在键值对不存在时,才会进行设置,否则不做赋值操作。此外,SET命令在执行时还可以带上EX或PX选项,用来设置键值对的过期时间。

加锁代码实现

举个例子,执行下面的命令时,只有key不存在时,SET才会创建key,并对key进行赋值。另外,key的存活时间由seconds或者milliseconds选项值来决定。

set key value  [EX seconds | PX milliseconds] [NX]

有了SET命令的NX和EX/PX选项后,我们就可以用下面的命令来实现加锁操作了。

//加锁,unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX1000a

● unique_value是客户端的唯一标识,可以用一个随机生成的字符串来表示

● PX 10000则表示lock_key会在10s后过期,以免客户端在这期间发生异常而无法释放锁。

释放锁代码实现

因为在加锁操作中,每个客户端都使用了一个唯一标识,所以在释放锁操作时,需要判断锁变量的值,是否等于执行释放锁操作的客户端的唯一标识,如下所示:

//释放锁比较unique_value是否相等
if redis.call("get",KEYS[1]==ARGV[1] then)
	return redis.call("del",keys[1])
else
	return 0
end

这是使用Lua脚本(unlock.script)实现的释放锁操作的伪代码,其中,KEYS[1]表示lock_key,ARGV[1]是当前客户端的唯一标识,这两个值都是在执行Lua脚本时作为参数传入的。

最后执行下面的命令,完成锁释放操作。

redis -cil --eval unlock.script lock_key,unique_value

在释放锁操作中使用Lua脚本—>这是因为,释放锁操作的逻辑也包含了读取锁变量、判断值、删除锁变量的多个操作,而Redis在执行Lua脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

2.5缺点:

使用SET命令和Lua脚本在Redis单节点上实现分布式锁。用一个Redis实例来保存锁变量,如果这个Redis实例发生故障宕机,锁变量就没有l 。此时,客户端也无法进行锁操作了,这就会影响到业务的正常执行,在实现分布式锁时,还需要保证锁的可靠性

3.基于多个Redis节点实现高可靠的分布式锁

实现高可靠的分布式锁时,不能只依赖单个的命令操作,需要按照分布式锁的算法进行加解锁操作,否则,就可能会出现锁无法工作的情况。

3.1分布式锁算法Redlock

分布式锁算法Redlock目的:

为了避免Redis实例故障而导致的锁无法工作的问题,

Redlock算法的基本思路:

让客户端和多个独立的Redis实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,客户端就成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个Redis实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

Redlock算法的执行步骤

Redlock算法的实现需要有N个独立的Redis实例。接下来,分成3步来完成加锁操作。

  1. 第一步是,客户端获取当前时间。
  2. 第二步是,客户端按顺序依次向N个Redis实例执行加锁操作。

这里的加锁操作和在单实例上执行的加锁操作一样,使用SET命令,带上NX,EX/PX选项,以及带上客户端的唯一标识。当然,如果某个Redis实例发生故障了,为了保证在这种情况下,Redlock算法能够继续运行,需要给加锁操作设置一个超时时间。

如果客户端在和一个Redis实例请求加锁时,一直到超时都没有成功,那么此时,客户端会和下一个Redis实例继续请求加锁。加锁操作的超时时间需要远远地小于锁的有效时间,一般也就是设置为几十毫秒。

第三步是,一旦客户端完成了和所有Redis实例的加锁操作,客户端就要计算整个加锁过程的总耗时。

客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

● 条件一:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;

● 条件二:客户端获取锁的总耗时没有超过锁的有效时间。

在满足了这两个条件后,重新计算这把锁的有效时间,计算的结果:锁的最初有效时间-客户端为获取锁的总耗时。

● 如果锁的有效时间已经来不及完成共享数据的操作–>释放锁,以免出现还没完成数据操作,锁就过期了的情况。

● 当然,如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有Redis节点发起释放锁的操作。

在Redlock算法中,释放锁的操作和在单实例上释放锁的操作一样,只要执行释放锁的Lua脚本就可以了。这样一来,只要N个Redis实例中的半数以上实例能正常工作,就能保证分布式锁的正常工作了。

所以,在实际的业务应用中,提升分布式锁的可靠性,通过Redlock算法来实现。

4.小结:

分布式锁是由共享存储系统维护的变量,多个客户端可以向共享存储系统发送命令进行加锁或释放锁操作。

Redis作为一个共享存储系统,可以用来实现分布式锁。

在基于单个Redis实例实现分布式锁时,对于加锁操作,需要满足三个条件。

1.加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,使用SET命令带上NX(不存在就设置)选项来实现加锁;

2.锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,在SET命令执行时加上EX/PX(设置过期)选项,设置其过期时间;

3.锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,使用SET命令设置锁变量值时,每个客户端设置的值是一个唯一值,用手标识客户端unique_value。

和加锁类似,释放锁也包含了读取锁变量值、判断锁变量值和删除锁变量三个操作,无法使用单个命令来实现,所以,采用Lua脚本执行释放锁操作。通过Redis原子性地执行Lua脚本,来保证释放锁操作的原子性。

不过,基于单个Redis实例实现分布式锁时,会面临实例异常或崩溃的情况,这会导致实例无法提供锁操作,正因为此,Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁,锁变量由多个实例维护,即使有实例发生了故障、锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决对案。

使用 SET 命令带上 NX 和 EX/PX 选项进行加锁操作,是否可以用下面的方式来实现加锁操作呢?

// 加锁
SETNX lock_key unique_value
EXPIRE lock_key 10S
// 业务逻辑
DO THINGS

答案:如果使用这个方法实现加锁的话,SETNX 和 EXPIRE 两个命令虽然分别完成了对锁变量进行原子判断和值设置,以及设置锁变量的过期时间的操作,但是这两个操作一起执行时,并没有保证原子性。如果在执行了 SETNX 命令后,客户端发生了故障,但锁变量还没有设置过期时间,就无法在实例上释放了,这就会导致别的客户端无法执行加锁操作。所以,我们不能使用这个方法进行加锁。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK