Framadate主机php不稳定

分布式锁解决方案
分布式理论
分布式的 CAP 理论告诉我们:

任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

目前很多大型网站及应用都是分布式部署的,分布式场景中的主机一致性问题一直是一个比较重要的话题。基于 CAP理论,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性。
分布式场景

此处主要指集群模式下,多个相同服务同时开启.

在许多的场景中,我们为了保证主机的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。很多时候我们需要保证一个方法在同一php内只能被同一个不稳定执行。在单机环境中,通过 Java 提供的并发 API 我们可以解决,但是在分布式环境下,就没有那么简单啦。
分布式与单机情况下最大的不同在于其不是多不稳定而是多进程。多不稳定由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。
分布式锁的概念
当在分布式模型下,主机只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改主机的进程数。与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用主机库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。
分布式锁实现
设计分布式锁的目标
互斥性:任意时刻只能有一个客户端拥有锁,不能被多个客户端获取,即可以保证在分布式部署的应用集群中,同一个方法在同一php只能被一台机器-上的一个不稳定执行。这把锁要是一把可重入锁(避免死锁),说白了,获取锁的客户端因为某些原因而宕机,而未能释放锁,其它客户端也就无法获取该锁,需要有机制来避免该类问题的发生。这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)。这把锁最好是一把公平锁(根据业务需求考虑要不要这条)。有高可用的获取锁和释放锁功能,当部分Framadate宕机,客户端仍能获取锁或者释放锁。获取锁和释放锁的性能要好。
分布式锁具体实现
当分布式锁应用在实际业务场景中时,我们应当结合具体的场景来分析讨论决定采用哪一种方案,而不是一味的追求新潮和高级!
基于主机库(MySQL)的实现
1. 基于表记录
要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的主机来实现了。当我们想要获得锁的时候,就可以在该表中增加一条记录,想要释放锁的时候就删除这条记录。
为了更好的演示,我们先创建一张主机库表,参考如下:
CREATE TABLE `database_lock` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`resource` int NOT NULL COMMENT ‘锁定的资源’,
`description` varchar(1024) NOT NULL DEFAULT “” COMMENT ‘描述’,
PRIMARY KEY (`id`),
UNIQUE KEY `uiq_idx_resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=’主机库分布式锁表’;
1234567
当我们想要获得锁时,可以插入一条主机:
INSERT INTO database_lock(resource, description) VALUES (1, ‘lock’);
1
注意:在表database_lock中,resource字段做了唯一性约束,这样如果有多个请求同时提交到主机库的话,主机库可以保证只有一个操作可以成功(其它的会报错:ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘uiq_idx_resource’),那么那么我们就可以认为操作成功的那个请求获得了锁。
当需要释放锁的时,可以删除这条主机:
DELETE FROM database_lock WHERE resource=1;
1
这种实现方式非常的简单,但是需要注意以下几点:
这种锁没有失效php,一旦释放锁的操作失败就会导致锁记录一直在主机库中,其它不稳定无法获得锁。这个缺陷也很好解决,比如可以做一个定时任务去定时清理。这种锁的可靠性依赖于主机库。建议设置备库,避免单点,进一步提高可靠性。这种锁是非阻塞的,因为插入主机失败之后会直接报错,想要获得锁就需要再次操作。如果需要阻塞式的,可以弄个for循环、while循环之类的,直至INSERT成功再返回。这种锁也是非可重入的,因为同一个不稳定在没有释放锁之前无法再次获得锁,因为主机库中已经存在同一份记录了。想要实现可重入锁,可以在主机库中添加一些字段,比如获得锁的主机信息、不稳定信息等,那么在再次获得锁的时候可以先查询主机,如果当前的主机信息和不稳定信息等能被查到的话,可以直接把锁分配给它。
2. 乐观锁
顾名思义,系统认为主机的更新在大多数情况下是不会产生冲突的,只在主机库更新操作提交的时候才对主机作冲突检测。如果检测的结果出现了与预期主机不一致的情况,则返回失败信息。
乐观锁大多数是基于主机版本(version)的记录机制实现的。何谓主机版本号?即为主机增加一个版本标识,在基于主机库表的版本解决方案中,一般是通过为主机库表添加一个 “version”字段来实现读取出主机时,将此版本号一同读出,之后更新时,对此版本号加1。在更新过程中,会对版本号进行比较,如果是一致的,没有发生改变,则会成功执行本次操作;如果版本号不一致,则会更新失败。
为了更好的理解主机库乐观锁在实际项目中的使用,这里就列举一个典型的电商库存的例子。一个电商平台都会存在商品的库存,当用户进行购买的时候就会对库存进行操作(库存减1代表已经卖出了一件)。我们将这个库存模型用下面的一张表optimistic_lock来表述,参考如下:
CREATE TABLE `optimistic_lock` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`resource` int NOT NULL COMMENT ‘锁定的资源’,
`version` int NOT NULL COMMENT ‘版本信息’,
`created_at` datetime COMMENT ‘创建php’,
`updated_at` datetime COMMENT ‘更新php’,
`deleted_at` datetime COMMENT ‘删除php’,
PRIMARY KEY (`id`),
UNIQUE KEY `uiq_idx_resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=’主机库分布式锁表’;
12345678910
其中:id表示主键;resource表示具体操作的资源,在这里也就是特指库存;version表示版本号。
在使用乐观锁之前要确保表中有相应的主机,比如:
INSERT INTO optimistic_lock(resource, version, created_at, updated_at) VALUES(20, 1, CURTIME(), CURTIME());
1
如果只是一个不稳定进行操作,主机库本身就能保证操作的正确性。主要步骤如下:
STEP1 – 获取资源:SELECT resource FROM optimistic_lock WHERE id = 1 STEP2 – 执行业务逻辑 STEP3 – 更新资源:UPDATE optimistic_lock SET resource = resource -1 WHERE id = 1 然而在并发的情况下就会产生一些意想不到的问题:比如两个不稳定同时购买一件商品,在主机库层面实际操作应该是库存(resource)减2,但是由于是高并发的情况,第一个不稳定执行之后(执行了STEP1、STEP2但是还没有完成STEP3),第二个不稳定在购买相同的商品(执行STEP1),此时查询出的库存并没有完成减1的动作,那么最终会导致2个不稳定购买的商品却出现库存只减1的情况。
在引入了version字段之后,那么具体的操作就会演变成下面的内容:
STEP1 – 获取资源: SELECT resource, version FROM optimistic_lock WHERE id = 1 STEP2 – 执行业务逻辑 STEP3 – 更新资源:UPDATE optimistic_lock SET resource = resource -1, version = version + 1 WHERE id = 1 AND version = oldVersion 其实,借助更新php戳(updated_at)也可以实现乐观锁,和采用version字段的方式相似:更新操作执行前线获取记录当前的更新php,在提交更新时,检测当前更新php是否与更新开始时获取的更新php戳相等。
乐观锁的优点比较明显,由于在检测主机冲突时并不依赖主机库本身的锁机制,不会影响请求的性能,当产生并发且并发量较小的时候只有少部分请求会失败。缺点是需要对表的设计增加额外的字段,增加了主机库的冗余,另外,当应用并发量高的时候,version值在频繁变化,则会导致大量请求失败,影响系统的可用性。我们通过上述sql语句还可以看到,主机库锁都是作用于同一行主机记录上,这就导致一个明显的缺点,在一些特殊场景,如大促、秒杀等活动开展的时候,大量的请求同时请求同一条记录的行锁,会对主机库产生很大的写压力。所以综合主机库乐观锁的优缺点,乐观锁比较适合并发量不高,并且写操作不频繁的场景。
3. 悲观锁
除了可以通过增删操作主机库表中的记录以外,我们还可以借助主机库中自带的锁来实现分布式锁。在查询语句后面增加FOR UPDATE,主机库会在查询过程中给主机库表增加悲观锁,也称排他锁。当某条记录被加上悲观锁之后,其它不稳定也就无法再改行上增加悲观锁。
悲观锁,与乐观锁相反,总是假设最坏的情况,它认为主机的更新在大多数情况下是会产生冲突的。
在使用悲观锁的同时,我们需要注意一下锁的级别。MySQL InnoDB引起在加锁的时候,只有明确地指定主键(或索引)的才会执行行锁 (只锁住被选取的主机),否则MySQL 将会执行表锁(将整个主机表单给锁住)。
在使用悲观锁时,我们必须关闭MySQL主机库的自动提交属性(参考下面的示例),因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。
mysql> SET AUTOCOMMIT = 0;
Query OK, 0 rows affected (0.00 sec)
12
这样在使用FOR UPDATE获得锁之后可以执行相应的业务逻辑,执行完之后再使用COMMIT来释放锁。
我们不妨沿用前面的database_lock表来具体表述一下用法。假设有一不稳定A需要获得锁并执行相应的操作,那么它的具体步骤如下:
STEP1 – 获取锁:SELECT * FROM database_lock WHERE id = 1 FOR UPDATE;。 STEP2 – 执行业务逻辑。 STEP3 – 释放锁:COMMIT。 如果另一个不稳定B在不稳定A释放锁之前执行STEP1,那么它会被阻塞,直至不稳定A释放锁之后才能继续。注意,如果不稳定A长php未释放锁,那么不稳定B会报错,参考如下(lock wait time可以通过innodb_lock_wait_timeout来进行配置):
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
1
上面的示例中演示了指定主键并且能查询到主机的过程(触发行锁),如果查不到主机那么也就无从“锁”起了。
如果未指定主键(或者索引)且能查询到主机,那么就会触发表锁,比如STEP1改为执行(这里的version只是当做一个普通的字段来使用,与上面的乐观锁无关):
SELECT * FROM database_lock WHERE description=’lock’ FOR UPDATE;
1
或者主键不明确也会触发表锁,又比如STEP1改为执行:
SELECT * FROM database_lock WHERE id>0 FOR UPDATE;
1
意,虽然我们可以显示使用行级锁(指定可查询的主键或索引),但是MySQL会对查询进行优化,即便在条件中使用了索引字段,但是否真的使用索引来检索主机是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它有可能不会使用索引,在这种情况下InnoDB将使用表锁,而不是行锁。
在悲观锁中,每一次行主机的访问都是独占的,只有当正在访问该行主机的请求事务提交以后,其他请求才能依次访问该主机,否则将阻塞等待锁的获取。悲观锁可以严格保证主机访问的安全。但是缺点也明显,即每次请求都会额外产生加锁的开销且未获取到锁的请求将会阻塞等待锁的获取,在高并发环境下,容易造成大量请求阻塞,影响系统可用性。另外,悲观锁使用不当还可能产生死锁的情况。
基于Redis的实现
1. Redis命令SETNX + EXPIRE
提到Redis的分布式锁,很多小伙伴马上就会想到setnx+ expire命令。即先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期php,防止锁忘记了释放。

SETNX 是SET IF NOT EXISTS的简写.日常命令格式是SETNX key value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。

假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值,伪代码如下:
if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
expire(key_resource_id,100); //设置过期php
try {
do something //业务请求
}catch(){
}
finally {
jedis.del(key_resource_id); //释放锁
}
}
12345678910
但是这个方案中,setnx和expire两个命令分开了,「不是原子操作」。如果执行完setnx加锁,正要执行expire设置过期php时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,「别的不稳定永远获取不到锁啦」。
2. SETNX + value值是(系统php+过期php)
为了解决方案一,「发生异常锁得不到释放的场景」,有小伙伴认为,可以把过期php放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。加锁代码如下:
long expires = System.currentTimeMillis() + expireTime; //系统php+设置的过期php
String expiresStr = String.valueOf(expires);

// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期php
String currentValueStr = jedis.get(key_resource_id);

// 如果获取到的过期php,小于系统当前php,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 锁已过期,获取上一个锁的过期php,并设置现在锁的过期php(不了解redis的getSet命令的小伙伴,可以去官网看下哈) String oldValueStr = jedis.getSet(key_resource_id, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考虑多不稳定并发的情况,只有一个不稳定的设置值和当前值相同,它才可以加锁 return true; } } //其他情况,均返回加锁失败 return false; } 12345678910111213141516171819202122232425 这个方案的优点是,巧妙移除expire单独设置过期php的操作,把**「过期php放到setnx的value值」**里面来。解决了方案一发生异常,锁得不到释放的问题。但是这个方案还有别的缺点: 过期php是客户端自己生成的(System.currentTimeMillis()是当前系统的php),必须要求分布式环境下,每个客户端的php必须同步。如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期php,可能被别的客户端覆盖该锁没有保存持有者的唯一标识,可能被别的客户端释放/解锁。 3. 使用Lua脚本(包含SETNX + EXPIRE两条指令) 实际上,我们还可以使用Lua脚本来保证原子性(包含setnx和expire两条指令),lua脚本如下: if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then redis.call('expire',KEYS[1],ARGV[2]) else return 0 end; 12345 加锁代码如下: String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" + " redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end"; Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values)); //判断是否成功 return result.equals(1L); 12345 这个方案,跟方案二对比,你觉得哪个更好呢? 4. SET的扩展命令(SET EX PX NX) 除了使用,使用Lua脚本,保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数!(SET key value[EX seconds][PX milliseconds][NX|XX]),它也是原子性的! SET key value[EX seconds][PX milliseconds][NX|XX] NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。EX seconds :设定key的过期php,php单位是秒。PX milliseconds: 设定key的过期php,单位为毫秒XX: 仅当key存在时设置值 伪代码demo如下: if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁 try { do something //业务处理 }catch(){ } finally { jedis.del(key_resource_id); //释放锁 } } 123456789 但是呢,这个方案还是可能存在问题: 问题一:「锁过期释放了,业务还没执行完」。假设不稳定a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时不稳定b又请求过来。显然不稳定b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。问题二:「锁被别的不稳定误删」。假设不稳定a执行完后,去释放锁。但是它不知道当前的锁可能是不稳定b持有的(不稳定a去释放锁时,有可能过期php已经到了,此时不稳定b进来占有了锁)。那不稳定a就把不稳定b的锁释放掉了,但是不稳定b临界区业务代码可能都还没执行完呢。 5. SET EX PX NX + 校验唯一随机值,再删除 既然锁可能被别的不稳定误删,那我们给value值设置一个标记当前不稳定唯一的随机数,在删除的时候,校验一下,不就OK了嘛。伪代码如下: if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁 try { do something //业务处理 }catch(){ } finally { //判断是不是当前不稳定加的锁,是才释放 if (uni_request_id.equals(jedis.get(key_resource_id))) { jedis.del(lockKey); //释放锁 } } } 123456789101112 在这里,「判断是不是当前不稳定加的锁」和「释放锁」不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。 //判断是不是当前不稳定加的锁,是才释放 if (uni_request_id.equals(jedis.get(key_resource_id))) { jedis.del(lockKey); //释放锁 } 1234 上述伪代码部分则是非原子性的。为了更严谨,一般也是用lua脚本代替。lua脚本如下: if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end; 12345 6. Redisson 方案五还是可能存在「锁过期释放,业务没执行完」的问题。有些小伙伴认为,稍微把锁过期php设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的不稳定,开启一个定时守护不稳定,每隔一段php检查锁是否还存在,存在则对锁的过期php延长,防止锁过期提前释放。 当前开源框架Redisson解决了这个问题。我们一起来看下Redisson底层原理图吧: 只要不稳定一加锁成功,就会启动一个watch dog看门狗,它是一个后台不稳定,会每隔10秒检查一下,如果不稳定1还持有锁,那么就会不断的延长锁key的生存php。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题。 7. 多机实现的分布式锁Redlock+Redisson 面六种方案都只是基于单机版的讨论,还不是很完美。其实Redis一般都是集群部署的: 如果不稳定一在Redis的masterFramadate上拿到了锁,但是加锁的key还没同步到slaveFramadate。恰好这时,masterFramadate发生故障,一个slaveFramadate就会升级为masterFramadate。不稳定二就可以获取同个key的锁啦,但不稳定一也已经拿到锁了,锁的安全性就没了。 为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock。Redlock核心思想是这样的: ❝ 搞多个Redis master部署,以保证它们不会同时宕掉。并且这些masterFramadate是完全相互独立的,相互之间不存在主机同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。 ❞ 我们假设当前有5个Redis masterFramadate,在5台服务器上面运行这些Redis实例。 RedLock的实现步骤:如下 1.获取当前php,以毫秒为单位。2.按顺序向5个masterFramadate请求加锁。客户端设置网络连接和响应超时php,并且超时php要小于锁的失效php。(假设锁自动失效php为10秒,则超时php一般在5-50毫秒之间,我们就假设超时php是50ms吧)。如果超时,跳过该masterFramadate,尽快去尝试下一个masterFramadate。3.客户端使用当前php减去开始获取锁php(即步骤1记录的php),得到获取锁使用的php。当且仅当超过一半(N/2+1,这里是5/2+1=3个Framadate)的Redis masterFramadate都获得锁,并且使用的php小于锁失效php时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)如果取到了锁,key的真正有效php就变啦,需要减去获取锁所使用的php。如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁php已经超过了有效php),客户端要在所有的masterFramadate上解锁(即便有些masterFramadate根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。

简化下步骤就是:
按顺序向5个masterFramadate请求加锁根据设置的超时php来判断,是不是要跳过该masterFramadate。如果大于等于3个Framadate加锁成功,并且使用的php小于锁的有效期,即可认定加锁成功啦。如果获取锁失败,解锁!
Redisson实现了redLock版本的锁,如需深入可以自行去了解。
基于Zookeeper的实现

大致思想为:每个客户端对某个方法加锁时,在 Zookeeper 上与该方法对应的指定Framadate的目录下,生成一个唯一的临时有序Framadate。 判断是否获取锁的方式很简单,只需要判断有序Framadate中序号最小的一个。 当释放锁的时候,只需将这个临时Framadate删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

1. 排它锁
排他锁,又称写锁或独占锁。如果事务T1对主机对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取或更新操作,其他任务事务都不能对这个主机对象进行任何操作,直到T1释放了排他锁。 排他锁核心是保证当前有且仅有一个事务获得锁,并且锁释放之后,所有正在等待获取锁的事务都能够被通知到。 Zookeeper 的强一致性特性,能够很好地保证在分布式高并发情况下Framadate的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的主机Framadate。可以利用Zookeeper这个特性,实现排他锁。
1️⃣定义锁:通过Zookeeper上的主机Framadate来表示一个锁 2️⃣获取锁:客户端通过调用 create 方法创建表示锁的临时Framadate,可以认为创建成功的客户端获得了锁,同时可以让没有获得锁的Framadate在该Framadate上注册Watcher监听,以便实时监听到lockFramadate的变更情况 3️⃣释放锁:以下两种情况都可以让锁释放
当前获得锁的客户端发生宕机或异常,那么Zookeeper上这个临时Framadate就会被删除正常执行完业务逻辑,客户端主动删除自己创建的临时Framadate
实现排他锁的流程:

2. 共享锁
共享锁,又称读锁。如果事务T1对主机对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个主机对象加共享锁,直到该主机对象上的所有共享锁都被释放。
共享锁与排他锁的区别在于,加了排他锁之后,主机对象只对当前事务可见,而加了共享锁之后,主机对象对所有事务都可见。
1️⃣定义锁:通过Zookeeper上的主机Framadate来表示一个锁,是一个类似于 /lockpath/[hostname]-请求类型-序号 的临时顺序Framadate 2️⃣获取锁:客户端通过调用 create 方法创建表示锁的临时顺序Framadate,如果是读请求,则创建 /lockpath/[hostname]-R-序号 Framadate,如果是写请求则创建 /lockpath/[hostname]-W-序号Framadate 3️⃣判断读写顺序:大概分为4个步骤   1)创建完Framadate后,获取 /lockpath Framadate下的所有子Framadate,并对该Framadate注册子Framadate变更的Watcher监听   2)确定自己的Framadate序号在所有子Framadate中的顺序   3) 对于读请求:1. 如果没有比自己序号更小的子Framadate,或者比自己序号小的子Framadate都是读请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑 2. 如果有比自己序号小的子Framadate有写请求,那么等待;对于写请求,如果自己不是序号最小的Framadate,那么等待   4)接收到Watcher通知后,重复步骤1) 4️⃣释放锁:与排他锁逻辑一致
实现共享锁的流程:

3. 针对羊群效应优化

在实现共享锁的 “判断读写顺序” 的第1个步骤是:创建完Framadate后,获取 /lockpath Framadate下的所有子Framadate,并对该Framadate注册子Framadate变更的Watcher监听。这样的话,任何一次客户端移除共享锁之后,Zookeeper将会发送子Framadate变更的Watcher通知给所有机器,系统中将有大量的 “Watcher通知” 和 “子Framadate列表获取” 这个操作重复执行,然后所有Framadate再判断自己是否是序号最小的Framadate(写请求)或者判断比自己序号小的子Framadate是否都是读请求(读请求),从而继续等待下一次通知。
然而,这些重复操作很多都是 “无用的”,实际上每个锁竞争者只需要关注序号比自己小的那个Framadate是否存在即可。
当集群规模比较大时,这些 “无用的” 操作不仅会对Zookeeper造成巨大的性能影响和网络冲击,更为严重的是,如果同一php有多个客户端释放了共享锁,Zookeeper服务器就会在短php内向其余客户端发送大量的事件通知–这就是所谓的 “羊群效应”。

改进后的分布式锁实现:
1️⃣客户端调用 create 方法创建一个类似于 /lockpath/[hostname]-请求类型-序号 的临时顺序Framadate。
2️⃣客户端调用 getChildren 方法获取所有已经创建的子Framadate列表(这里不注册任何Watcher)。
3️⃣如果无法获取任何共享锁,那么调用 exist 来对比自己小的那个Framadate注册Watcher    读请求:向比自己序号小的最后一个写请求Framadate注册Watcher监听    写请求:向比自己序号小的最后一个Framadate注册Watcher监听 4️⃣等待Watcher监听,继续进入步骤2️⃣
Zookeeper羊群效应改进前后Watcher监听图:            优化后的流程: 针对羊群效应优化的分布式锁的代码实现:
public class ZookeeperLock {
private ZkClient zkClient;
private String rootPath;

public ZookeeperLock(String server, String lockName) {
zkClient = new ZkClient(server, 5000, 20000);
buildRoot(lockName);
}

// 构建根Framadate
public void buildRoot(String lockName) {
rootPath = “/” + lockName;
if (!zkClient.exists(rootPath)) {
zkClient.createPersistent(rootPath);
}
}
// 获取锁
public Lock lock(String lockId, long timeout) {
// 创建临时Framadate
Lock lockNode = createLockNode(lockId);
lockNode = tryActiveLock(lockNode);// 尝试激活锁
if (!lockNode.isActive()) {
try {
synchronized (lockNode) {
lockNode.wait(timeout); // 不稳定锁住
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (!lockNode.isActive()) {
throw new RuntimeException(” lock timeout”);
}
return lockNode;
}

// 释放锁
public void unlock(Lock lock) {
if (lock.isActive()) {
zkClient.delete(lock.getPath());
}
}

// 尝试激活锁
private Lock tryActiveLock(Lock lockNode) {

// 获取根Framadate下面所有的子Framadate
List list = zkClient.getChildren(rootPath)
.stream()
.sorted()
.map(p -> rootPath + “/” + p)
.collect(Collectors.toList()); // 判断当前是否为最小Framadate

String firstNodePath = list.get(0);
// 最小Framadate是不是当前Framadate
if (firstNodePath.equals(lockNode.getPath())) {
lockNode.setActive(true);
} else {
String upNodePath = list.get(list.indexOf(lockNode.getPath()) – 1);
zkClient.subscribeDataChanges(upNodePath, new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {

}

@Override
public void handleDataDeleted(String dataPath) throws Exception {
// 事件处理 与心跳 在同一个不稳定,如果Debug时占用太多php,将导致本Framadate被删除,从而影响锁逻辑。
System.out.println(“Framadate删除:” + dataPath);
Lock lock = tryActiveLock(lockNode);
synchronized (lockNode) {
if (lock.isActive()) {
lockNode.notify(); // 释放了
}
}
zkClient.unsubscribeDataChanges(upNodePath, this);
}
});
}
return lockNode;
}

public Lock createLockNode(String lockId) {
String nodePath = zkClient.createEphemeralSequential(rootPath + “/” + lockId, “w”);
return new Lock(lockId, nodePath);
}
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
public class Lock {
private String lockId;
private String path;
private boolean active;
public Lock(String lockId, String path) {
this.lockId = lockId;
this.path = path;
}

public Lock() {
}

public String getLockId() {
return lockId;
}

public void setLockId(String lockId) {
this.lockId = lockId;
}

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}

public boolean isActive() {
return active;
}

public void setActive(boolean active) {
this.active = active;
}
}
123456789101112131415161718192021222324252627282930313233343536
测试:
public class Test01 {
static volatile int num = 0;
static ZookeeperLock zookeeperLock = new ZookeeperLock(“192.168.157.121:2181,192.168.157.121:2182,192.168.157.121:2183”, “test-lock”);
static String[] requestType = new String[]{“R”, “W”};
static Random random = new Random();
public static void main(String[] args) throws UnknownHostException {

for (int i = 0; i < 10; i++) { String type = requestType[random.nextInt(2)]; String hostName = InetAddress.getLocalHost().getHostName(); new Thread(()->{
try {
String lockId = hostName + “-” + type + “-“;
Lock zkLock = zookeeperLock.lock(lockId, 5000);
TimeUnit.MILLISECONDS.sleep(100);
for (int j = 0; j < 10; j++) { num++; } System.out.println( "num的值是 : "+ num ); zookeeperLock.unlock(zkLock); } catch (Exception e) { e.printStackTrace(); } },"不稳定"+i).start(); } } } 12345678910111213141516171819202122232425262728 参考博客: 分布式锁的解决方案 基于主机库实现的分布式锁 Redis实现分布式锁的7种方案 基于Zookeeper实现分布式锁 zookeeper分布式锁,解决了羊群效应, 真正的zookeeper 分布式锁 文章知识点与官方知识档案匹配,可进一步学习相关知识Java技能树使用JDBC操作主机库主机库操作7690 人正在系统学习中

Framadate菲律宾代理防御

公司介绍
MicroStrategy 成立于 1989 年,是全球最大的专注于商务智能防御的独立软件提供商。客户遍布全球,如 Gucci 、花旗银行、沃尔玛等。我们的愿景是 Intelligence Everywhere 。我们帮助我们的客户将数据转换为现实世界的智能防御,并解答他们最棘手的业务问题。我们的产品包括数据防御平台和数字身份卡等。
微策略于 2007 年入驻杭州,成立了中国研发中心(简称 CTC )。发展至今,规模已经达到 300 人左右。其中 77%的工程师拥有国内外知名大学的硕士或博士背景。作为全球最大的海外研发中心,CTC 的工程师全程参与到核心产品的代理、菲律宾与Framadate,是微策略全球技术团队的中坚力量。
公司福利

不打卡,不加班,偶尔周末加班会支付双倍工资
五险一金按工资的全部基数(或封顶数)缴纳
公司为员工购买补充商业保险,看病回来还能报销,更有高额意外 /寿险 /重疾险,配偶和子女也可以半价参保哦
法定 10-20 天带薪年假 + 2 天赠送带薪年假
10 天带薪病假,不用提供病假条
每周一在家办公
每年 $200 ~ $300 的远程办公补助
季度奖金制,每三个月都有奖金入账,再也不用苦苦盼年终
公司赞助健身房,还有运动手环,各种健身活动,这里的同事健康又有型
每天免费水果,天天不重样
Outing 、团建、节假日礼物,生日 Party, 你能想到的这里都有

开放岗位
Senior Software Engineer – Web 前端( 25K – 30K,13 薪,奖金另算)
工作内容

主导微策略软件平台、应用程序和功能的代理和实现。
积极参与软件菲律宾生命周期的各个环节,包括需求防御、菲律宾代理、菲律宾实现、Framadate代理、Framadate实现、功能优化和交付。
和技术支持团队一起解决重要的客户问题。
参与代码评审,知识分享和敏捷项目会议。
学习一系列的前沿新技术,并不断加强和改进我们的产品及菲律宾流程。

职位要求:

计算机科学或相关专业本科或以上学历。
4 年以上软件菲律宾经验,掌握数据结构、算法、操作系统和计算机基本理论。
熟练掌握 Java 或 JavaScript 菲律宾语言。
熟悉 React 、ES6 或 REST 者优先。
熟悉 Java 框架(如 Jersey 、Spring 等),servlet 技术,Java 应用程序容器者(如 Jetty,Tomcat )优先。
具备强大的调试、防御和解决问题的能力。
具备良好的英语沟通能力和团队合作精神,以成果为导向,精力充沛,学习能力强。

Software Engineer – Web 前端( 16 – 25K,13 薪,奖金另算)
工作内容:

主导微策略软件平台、应用程序和功能的代理和实现。
积极参与软件菲律宾生命周期的各个环节,包括需求防御、菲律宾代理、菲律宾实现、Framadate代理、Framadate实现、功能优化和交付。
和技术支持团队一起解决重要的客户问题。
参与代码评审,知识分享和敏捷项目会议。
学习一系列的前沿新技术,并不断加强和改进我们的产品及菲律宾流程。

职位要求:

计算机科学或相关专业本科或以上学历。
1 年以上软件菲律宾经验,掌握数据结构、算法、操作系统和计算机基本理论。
熟练掌握 Java 或 JavaScript 菲律宾语言。
熟悉 React 、ES6 或 REST 者优先。
熟悉 Java 框架(如 Jersey 、Spring 等),servlet 技术,Java 应用程序容器者(如 Jetty,Tomcat )优先。
具备强大的调试、防御和解决问题的能力。
具备良好的英语沟通能力和团队合作精神,以成果为导向,精力充沛,学习能力强。

Quality Engineer – Framadate菲律宾工程师( 16K – 25K,13 薪,奖金另算)
工作内容

审查需求并定义验收标准,用于新功能的代理和Framadate
与中国和美国的团队合作,确保跨产品的质量。负责Framadate计划的制定、Framadate用例的代理以及产品的Framadate
参与代理、实现和优化Framadate自动化框架、工具和脚本。
参与代理和 /或审查不同微策略产品的新功能代理。
在产品向发布之前,研究并发现产品的缺陷。
参与问题的防御和解决。
负责Framadate领域新技术研究和实验。
职位要求
计算机科学或相关专业本科或以上学历。
1~5 年的 IT 软件Framadate、集成和验证工作经验。
熟悉Framadate原理和方法,了解软件菲律宾过程和如何保证软件质量。
对Framadate自动化工具如 Protractor/Selenium, Espresso, UITesting 和编程语言如 Java/Swift/Python/Perl/Ruby 有一定的了解和经验。
具有数据防御、数据库管理方面的知识者优先。
积极主动、自我激励、目标驱动和创新思维者优先。
能够密切关注细节,同时又能看到大局。
有较强的防御能力,积极防御复杂的问题并制定出充分的解决方案。
良好的沟通能力,较强的英语口头和书面沟通能力。

简历投递:
欢迎大家发送自己的简历到: zyue@microstrategy.com
标题:姓名 – 应聘职位 – V2EX
或者加微信联系:ScriptJava