分布式锁

考虑问题:分布式商品秒杀

在分布式部署中,以nginx提供负载均衡,提供服务主机以集群形式部署。mysql作为db服务。
比如:现在db中存储了1w+的商品信息,用于提供服务的主机集群里面包含三台高性能主机,预估此次参与秒杀的人有3w+,
秒杀活动9点中开始,nginx对外暴露的域名为 www.goodms.com

分析过程:

1. 在活动开始之后,由于nginx的负载均衡作用,3w+的人员被分流至三台主机上。
2. 为了实现高并发秒杀商品,在每一台主机上面再启500的线程,从db中获取商品,并进行商品获取之后的信息校验,商品删除的相关操作。
3. 由于启动的线程可能位于不同的主机上面,而jdk并发包提供的加锁手段此时将失效,因为加锁的实现是针对用一个JVM的。目前而言,db中的
数据为分布式环境中的共享数据,故引入分布式锁的概念。
4. 针对分布式锁的实现:目前主要存在三种技术:
a. 基于数据库的实现
b. 基于redis缓存的实现
c. 基于zk的实现

分布式锁的实现:

1. 基于数据库
基于数据库的实现,主要是在数据库中维持一个记录锁信息的表。该表中存在一个字段,且该字段的值时唯一确认,且不能重复,比如值为:lock。
当不同主机上面的线程需要秒杀商品时,首先去锁表里面插入lock值,如果可以插入成功,说明当前没有其他线程操作db,也就是当前线程获得了锁,比如处理商品相关服务(商品信息校验,商品数据删除等等)需要10s中,在这10s之内进入的线程首先去表里面插入lock值,由于该字段属性是唯一的,那么会出现插入失败,则认为获取锁失败。然后尝试再次获取锁,直到获取成功(前提是上一个锁拥线程删除锁表中的记录),才可以进行商品的相关操作。

缺点:如果锁拥有线程在执行业务逻辑中,突然宕机,那么锁表中的记录将永远无法清除,或造成死锁现象。可以通过设置过期时间来阻止该现象的发生。

2. 基于Redis
实现原理一样,缺点也是容易造成死锁现象,因为必须是在连接存在的情况下才能删除锁记录。可以通过设置过期时间来阻止该现象的发生。

3. 基于zk实现:
主要依赖的是临时节点的创建,临时节点会在连接关闭时自动删除,所以以临时节点作为锁,可以避免死锁现象。
注意:每一次获取锁时,都要进行连接的创建,因为释放锁的时候,节点已经关闭。

分布式锁可以适用于分布式场景,也可以适用单机部署场景,但是单机场景不建议使用分布式锁,因为分布式锁的实现是依赖连接的建立,会导致性能严重下降。

依赖:

1     <dependency>
2         <groupId>com.101tec</groupId>
3         <artifactId>zkclient</artifactId>
4         <version>0.10</version>
5     </dependency>
View Code

分布式锁:

package com.example.demo.test;

import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;

import java.util.concurrent.CountDownLatch;

/**
 * Created by 156 on 2019/2/10.
 */
public class MyLocak {

    // zk连接地址
    private static final String CONNECTSTRING = "192.168.20.1:2181";
    // 创建zk连接
    protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
    //
    protected static final String PATH = "/lock";
    //  信号量
    private CountDownLatch countDownLatch = null;

    //获取到锁的资源
    public void getLock(){
        if (tryLock()) {
            System.out.println("##获取lock锁的资源####");
        } else {
            // 等待
            waitLock();
            // 重新获取锁资源
            getLock();
        }
    }
    // 释放锁
    public void unLock(){
        if (zkClient != null) {
            zkClient.close();
            System.out.println("释放锁...");
        }
    }

    private boolean tryLock() {
        try {
            // 创建临时节点
            zkClient.createEphemeral(PATH);
            return true;
        } catch (Exception e) {
//            e.printStackTrace();
            return false;
        }

    }
    private void waitLock() {
        IZkDataListener izkDataListener = new IZkDataListener() {

            public void handleDataDeleted(String path) throws Exception {
                // 唤醒被等待的线程
                if (countDownLatch != null) {
                    countDownLatch.countDown();
                }
            }
            public void handleDataChange(String path, Object data) throws Exception {

            }
        };
        // 注册事件
        zkClient.subscribeDataChanges(PATH, izkDataListener);
        if (zkClient.exists(PATH)) {
            countDownLatch = new CountDownLatch(1);
            try {
                countDownLatch.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 删除监听
        zkClient.unsubscribeDataChanges(PATH, izkDataListener);
    }
}
View Code

测试代码:

public class T {

    public static void main(String[] args) {
        for (int i=0;i<100;i++){
            // 每一次获取锁的连接都必须重建
            ThreadDemo demo = new ThreadDemo();
            new Thread(demo).start();
        }
    }
}
View Code

ThreadDemo代码:

public class ThreadDemo implements Runnable{
 
    //  在分布式场景中,该变量的存在用于标识网站的访问量
    static int count = 0;
 
    private Object lock = new Object();
 
    private MyLocak locak = new MyLocak();
    @Override
    public void run() {
      /*  synchronized (lock){
            System.out.println("当前累计访问人数:"+ ++count +" 人");
        }*/
       locak.getLock();
        System.out.println("当前累计访问人数:"+ ++count +" 人");
        locak.unLock();
    }
}
View Code

 

posted @ 2019-04-10 18:13  xinjia  阅读(146)  评论(0编辑  收藏  举报