资料来自MIC老师,仅学习参考。
理解分布式锁
我们先来看一个问题,如图11-1所示,两个用户同时去抢购秒杀商品,当秒杀服务同时收到秒杀请求时,都去进行库存扣减,此时在没有做任何处理的情况下,就会导致库存数量变成负数从而导致超卖现象。
这种情况下我们一般会选择加锁的方式来避免并发的问题。但是在分布式场景中,采用传统的锁并不能解决跨进程并发的问题,所以需要引入一个分布式锁,来解决多个节点之间的访问控制。
Zookeeper如何解决分布式锁
我们可以基于Zookeeper的两种特性来实现分布式锁,先来说第一种,
1、使用唯一节点特性实现分布式锁
就是基于唯一节点特性,如图11-2所示。多个应用程序去抢占锁资源时,只需要在指定节点上创建一个 /Lock 节点,由于Zookeeper中节点的唯一性特性,使得只会有一个用户成功创建 /Lock 节点,剩下没有创建成功的用户表示竞争锁失败。
这种方法能达到目的,但是会有一个问题,如图11-3所示,假设有非常多的节点需要等待获得锁,那么等待的方式自然是使用Watcher机制来监听/lock节点的删除事件,
一旦发现该节点被删除说明之前获得锁的节点已经释放了锁,此时剩下的B、C、D。。节点同时会收到删除事件从而去竞争锁,这个过程会产生惊群效应。
但是会产生“惊群效应”,简单来说就是如果存在许多的客户端在等待获取锁,当成功获取到锁的进程释放该节点后,所有处于等待状态的客户端都会被唤醒,这个时候zookeeper在短时间内发送大量子节点变更事件给所有待获取锁的客户端,然后实际情况是只会有一个客户端获得锁。如果在集群规模比较大的情况下,会对zookeeper服务器的性能产生比较的影响。
2、使用有序节点实现分布式锁
因此为了解决这个问题,我们可以采用Zookeeper的有序节点特性来实现分布式锁。
如图11-4所示,每个客户端都往指定的节点下注册一个临时有序节点,越早创建的节点,节点的顺序编号就越小,那么我们可以判断子节点中最小的节点设置为获得锁。如果自己的节点不是所有子节点中最小的,意味着还没有获得锁。这个的实现和前面单节点实现的差异性在于,每个节点只需要监听比自己小的节点,当比自己小的节点删除以后,客户端会收到watcher事件,此时再次判断自己的节点是不是所有子节点中最小的,如果是则获得锁,否则就不断重复这个过程,这样就不会导致羊群效应,因为每个客户端只需要监控一个节点。
如图11-5所示,表示有序节点实现分布式锁的流程。
Curator实现分布式锁
在本节中我们使用Curator来实现分布式锁。为了实现分布式锁,我们先演示一个存在并发异常的场景。
搭建一个商品抢购场景
sql脚本如下。
DROP TABLE IF EXISTS `goods_stock`;
CREATE TABLE `goods_stock` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`goods_no` int NOT NULL COMMENT '商品编号',
`stock` int DEFAULT NULL COMMENT '库存',
`isActive` smallint DEFAULT NULL COMMENT '是否上架(1上,0不是)',
PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
Controller代码
@Scope(scopeName = "prototype")
@RestController
@RequestMapping("/goods-stock")
public class GoodsStockController {
@Autowired
IGoodsStockService goodsStockService;
@Autowired
CuratorFramework curatorFramework;
@GetMapping("{goodsNo}")
public String purchase(@PathVariable("goodsNo")Integer goodsNo) throws Exception {
QueryWrapper<GoodsStock> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("goods_no",goodsNo);
//基于临时有序节点来实现的分布式锁.
// InterProcessMutex lock=new InterProcessMutex(curatorFramework,"/Locks");
try {
// lock.acquire(); //抢占分布式锁资源(阻塞的)
GoodsStock goodsStock = goodsStockService.getOne(queryWrapper);
Thread.sleep(new Random().nextInt(1000));
if (goodsStock == null) {
return "指定商品不存在";
}
if (goodsStock.getStock().intValue() < 1) {
return "库存不够";
}
goodsStock.setStock(goodsStock.getStock() - 1);
boolean res = goodsStockService.updateById(goodsStock);
if (res) {
return "抢购书籍:" + goodsNo + "成功";
}
}finally {
// lock.release(); //释放锁
}
return "抢购失败";
}
}
上述代码使用jmeter进行压测,用1500个线程,库存数量设置成100,监视数据库中库存的变化发现,整个库存变化过程是非常混乱的。
引入Zookeeper实现分布式锁
curator对于锁这块做了一些封装,curator提供了InterProcessMutex 这样一个api。除了分布式锁之外,还提供了leader选举、分布式队列等常用的功能。
-
InterProcessMutex:分布式可重入排它锁
-
InterProcessSemaphoreMutex:分布式排它锁
-
InterProcessReadWriteLock:分布式读写锁
具体的使用方法如下。
-
引入pom
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.0</version>
</dependency>
-
修改GoodsController,增加锁机制
@Scope(scopeName = "prototype")
@RestController
@RequestMapping("/goods-stock")
public class GoodsStockController {
@Autowired
IGoodsStockService goodsStockService;
@Autowired
CuratorFramework curatorFramework;
@GetMapping("{goodsNo}")
public String purchase(@PathVariable("goodsNo")Integer goodsNo) throws Exception {
QueryWrapper<GoodsStock> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("goods_no",goodsNo);
//基于临时有序节点来实现的分布式锁.
InterProcessMutex lock=new InterProcessMutex(curatorFramework,"/Locks");
try {
lock.acquire(); //抢占分布式锁资源(阻塞的)
GoodsStock goodsStock = goodsStockService.getOne(queryWrapper);
Thread.sleep(new Random().nextInt(1000));
if (goodsStock == null) {
return "指定商品不存在";
}
if (goodsStock.getStock().intValue() < 1) {
return "库存不够";
}
goodsStock.setStock(goodsStock.getStock() - 1);
boolean res = goodsStockService.updateById(goodsStock);
if (res) {
return "抢购书籍:" + goodsNo + "成功";
}
}finally {
lock.release(); //释放锁
}
return "抢购失败";
}
}
-
CuratorConfifig
@Configuration
public class CuratorConfig {
@Bean
public CuratorFramework curatorFramework(){
CuratorFramework curatorFramework=CuratorFrameworkFactory
.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(15000)
.connectionTimeoutMs(20000)
.retryPolicy(new ExponentialBackoffRetry(1000,10))
.build();
curatorFramework.start();
return curatorFramework;
}
}
-
继续通过jmeter压测,可以看到就不存在库存超卖问题了。
Curator实现分布式锁的源码分析
抢占锁的逻辑
Curator构造函数
acquire方法
调用acquire方法,该方法有两个重载方法,另外一个是带超时时间,当等待超时没有获得锁则放弃锁的占用。
internalLock
attemptLock
createsTheLock
// 在Zookeeper中创建临时顺序节点
internalLockLoop
driver.getsTheLock
锁撤销
InterProcessMutex支持一种协商撤销互斥锁的机制, 可以用于死锁的情况想要撤销一个互斥锁可以调用下面这个方法:
public void makeRevocable(RevocationListener<T> listener)
这个方法可以让锁持有者来处理撤销动作。 当其他进程/线程想要你释放锁时,就会回调参数中的监听器方法。 但是,此方法不是强制撤销的,是一种协商机制。
当想要去撤销/释放一个锁时,可以通过 Revoker 中的静态方法来发出请求,
Revoker.attemptRevoke();
public static void attemptRevoke(CuratorFramework client,String path) throws
Exception
-
path :加锁的zk节点path,通常可以通过 InterProcessMutex.getParticipantNodes() 获得这个方法会发出撤销某个锁的请求。如果锁的持有者注册了上述的 RevocationListener 监听器,那么就会调用监听器方法协商撤销锁。
使用Zookeeper实现多个节点的leader选举
在分布式计算中,leader election是很重要的一个功能,这个选举过程是这样子的:指派一个进程作为组织者,将任务分发给各节点。在任务开始前,哪个节点都不知道谁是leader或者coordinator。当选举算法开始执行后,每个节点最终会得到一个唯一的节点作为任务leader。除此之外,选举还经常会发生在leader意外宕机的情况下,新的leader要被选举出来,如图11-7所示,这个就是所谓的leader选举,而zookeeper作为leader选举的功能,在很多中间件中都有使用,比如kafka基于zookeeper实现leader选举,Hadoop、Spark等。
Curator实现leader选举
除了作为集群节点的leader选举之外,leader选举还可以用在其他的场景,比如在分布式调度任务系统中,从可靠性角度出发,集群也是必不可少的。但往往,为了保证任务不会重复分配,分配任务的节点只能有一个,这种情况就需要从集群中选出一个Leader(老大)去任务池里取任务,如图11-8所示。
本文就会介绍Curator基于Zookeeper封装的Leader选举工具类LeaderLatch与LeaderSelector的使用及原理分析,Curator有两种选举recipe(Leader Latch和Leader Election),两种实现机制上有一定的差异,后续会逐步说明。
LeaderLatch使用实战
Quartz中最重要的三个对象:Job、Trigger、Scheduler。
模拟定时任务调度,两台服务器只能有一台跑定时任务,多台服务器运行同样的定时任务,只有一台跑任务,当这台挂了,回暖选举出另外一台跑
-
Job,表示任务
-
Trigger,配置调度参数
-
Scheduler,代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger
public class GpLeaderLatchListener implements LeaderLatchListener {
//控制定时任务启动和停止的方法
private SchedulerFactoryBean schedulerFactoryBean;
public GpLeaderLatchListener(SchedulerFactoryBean schedulerFactoryBean) {
this.schedulerFactoryBean = schedulerFactoryBean;
}
@Override
public void isLeader() {
System.out.println(Thread.currentThread().getName()+"成为了leader");
schedulerFactoryBean.setAutoStartup(true);
schedulerFactoryBean.start();
}
@Override
public void notLeader() {
System.out.println(Thread.currentThread().getName()+"抢占leader失败,不执行任务");
schedulerFactoryBean.setAutoStartup(false);
schedulerFactoryBean.stop();
}
}
@Configuration
public class QuartzConfiguration {
@Bean
public ZkSchedulerFactoryBean schedulerFactoryBean(JobDetail jobDetail,Trigger trigger) throws Exception {
ZkSchedulerFactoryBean zkSchedulerFactoryBean=new ZkSchedulerFactoryBean();
zkSchedulerFactoryBean.setJobDetails(jobDetail);
zkSchedulerFactoryBean.setTriggers(trigger);
return zkSchedulerFactoryBean;
}
@Bean
public JobDetail jobDetail(){
return JobBuilder.newJob(QuartzJob.class).storeDurably().build();
}
@Bean
public Trigger trigger(JobDetail jobDetail){
SimpleScheduleBuilder simpleScheduleBuilder=
SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever();
return TriggerBuilder.newTrigger().forJob(jobDetail).withSchedule(simpleScheduleBuilder).build();
}
}
public class QuartzJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("开始执行定时任务");
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("当前执行的系统时间:"+sdf.format(new Date()));
}
}
public class ZkSchedulerFactoryBean extends SchedulerFactoryBean {
private LeaderLatch leaderLatch;
private final String LEADER_PATH="/leader"; //namespace
public ZkSchedulerFactoryBean() throws Exception {
this.setAutoStartup(false); //应用启动的时候不自动开启定时任务
leaderLatch=new LeaderLatch(getClient(),LEADER_PATH);
leaderLatch.addListener(new GpLeaderLatchListener(this)); //当leader发生变化的时候,需要触发监听
leaderLatch.start();
}
private CuratorFramework getClient(){
CuratorFramework curatorFramework= CuratorFrameworkFactory
.builder()
.connectString("192.168.221.128:2181")
.sessionTimeoutMs(15000)
.connectionTimeoutMs(20000)
.retryPolicy(new ExponentialBackoffRetry(1000,10))
.build();
curatorFramework.start();
return curatorFramework;
}
@Override
protected void startScheduler(Scheduler scheduler, int startupDelay) throws SchedulerException {
if(this.isAutoStartup()) {
super.startScheduler(scheduler, startupDelay);
}
}
@Override
public void destroy() throws SchedulerException {
CloseableUtils.closeQuietly(leaderLatch);
super.destroy();
}
}
LeaderSelector案例演示
LeaderSelector和Leader Latch最的差别在于,LeaderSelector可以释放领导权以后,还可以继续参与竞争。
public class SelectorClientExample extends LeaderSelectorListenerAdapter implements Closeable {
private final String name;
private final LeaderSelector leaderSelector;
public SelectorClientExample(String path,String name) {
// 利用一个给定的路径创建一个leader selector
// 执行leader选举的所有参与者对应的路径必须一样
// 本例中SelectorClient也是一个LeaderSelectorListener,但这不是必须的。
leaderSelector=new LeaderSelector(getClient(),path,this);
// 在大多数情况下,我们会希望一个selector放弃leader后还要重新参与leader选举
leaderSelector.autoRequeue();
this.name=name;
}
@Override
public void close() throws IOException {
leaderSelector.close();
}
public void start(){
leaderSelector.start();
}
@Override
public void takeLeadership(CuratorFramework client) throws Exception {
System.out.println(name+" 成为Leader");
Thread.sleep(1000);
}
private CuratorFramework getClient(){
CuratorFramework curatorFramework= CuratorFrameworkFactory
.builder()
.connectString("192.168.221.128:2181")
.sessionTimeoutMs(15000)
.connectionTimeoutMs(20000)
.retryPolicy(new ExponentialBackoffRetry(1000,10))
.build();
curatorFramework.start();
return curatorFramework;
}
public static void main(String[] args) throws IOException {
String path="/leader";
for (int i = 0; i < 10; i++) {
SelectorClientExample selectorClientExample=
new SelectorClientExample(path,"Client:"+i);
selectorClientExample.start();
}
//阻塞,让当前获得leader权限的节点一直持有,直到该进程关闭
System.in.read();
}
}