1.在高并发的情况下,如何高效的只允许一个线程修改一条记录
2.分布式情况下,怎么解决订单号生成不重复
为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用synchronized+lock实现加锁,解决高并发问题。synchronized+lock只能锁定单机对单个jvm加锁。
成员变量 A 存在 JVM1、JVM2、JVM3 三个 JVM 内存中
成员变量 A 同时都会在 JVM 分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的
不是同时发过来,三个请求分别操作三个不同 JVM 内存区域的数据,变量 A 之间不存在共享,也不具有可见性,处理的结果也是不对的
现在分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,要保证一个方法在同一时间只能被一个机器的一个线程执行的话,就要对多个jvm加锁。为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。
分布式锁应该具备哪些条件
在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
高可用的获取锁与释放锁
高性能的获取锁与释放锁
具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
具备锁失效机制,防止死锁
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。
分布式锁有三种方案:
1.数据库的乐观锁实现(并发量不够,别用)
2.redis(setnx
命令)、redisson(框架)
3.zookeeper(本次采用zookeeper)
设计思想
对于单进程的并发场景,我们可以使用synchronized关键字和Reentrantlock类等。
对于分布式场景,我们可以使用分布式锁。
创建锁
多个jvm服务器之间,同时在zookeeper上创建相同的一个临时节点,因为临时节点路径是保证唯一。
只要谁能够创建节点成功,谁就能够获取到锁。没有创建成功节点,只能注册个监听器监听这个锁并进行等待,当释放锁的时候,采用事件通知给其他客户端重新获取锁的资源。这时候客户端使用事件监听,如果该临时节点被删除的话,重新进入获取锁的步骤。
释放锁
Zookeeper使用直接关闭临时节点session会话连接,因为临时节点生命周期与session会话绑定在一块,如果session会话连接关闭的话,该临时节点也会被删除。这时候客户端使用事件监听,如果该临时节点被删除的话,重新进入到获取锁的步骤。
zookeeper
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。
那对于我们初次认识的人,可以理解成ZooKeeper就像是我们的电脑文件系统,我们可以在d盘中创建文件夹a,并且可以继续在文件夹a中创建 文件夹a1,a2。
那我们的文件系统有什么特点??那就是同一个目录下文件名称不能重复,同样ZooKeeper也是这样的。
在ZooKeeper所有的节点,也就是文件夹称作 Znode,而且这个Znode节点是可以存储数据的。
ZooKeeper可以创建4种类型的节点,分别是:
-
持久性节点
-
持久性顺序节点
-
临时性节点
-
临时性顺序节点
持久性节点和临时性节点的区别:
持久性节点表示只要你创建了这个节点,那不管你ZooKeeper的客户端是否断开连接,ZooKeeper的服务端都会记录这个节点。
临时性节点刚好相反,一旦你ZooKeeper客户端断开了连接,那ZooKeeper服务端就不再保存这个节点。
顺序性节点:
在创建节点的时候,ZooKeeper会自动给节点编号比如0000001 ,0000002 这种的。
最后说下,zookeeper有一个监听机制,客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)等,zookeeper会通知客户端。
代码实现
pom.xml导入zk客户端
<dependency> <groupId>com.101tec</groupId> <artifactId>zkclient</artifactId> <version>0.10</version> </dependency>
生成订单号自加:
public class OrderNumGeneratorUtil { private static int num = 0; public String getNumber(){ return "订单号为:" +(++num); } }
创建锁的接口
public interface ZkLock { //加锁 public void lock(); //释放锁 public void unLock(); }
模板方法模式
//将重复代码抽象到子类中(模板方法设计模式) public abstract class ZkAbstractLock implements ZkLock { private static final String CONNECTION="127.0.0.1:2181"; protected ZkClient zkClient = new ZkClient(CONNECTION); protected String lockPath="/zk_lock"; protected CountDownLatch countDownLatch=null; //获取锁 public void lock() { //如果节点创建成功,直接执行业务逻辑,如果节点创建失败,进行等待,递归调用lock if (tryLock()) { System.out.println("*************成功获取锁***************"); }else { //进行等待 waitLock(); lock(); } } abstract void waitLock(); abstract boolean tryLock(); //释放锁 public void unLock() { if (zkClient != null) { zkClient.close(); System.out.println("----------------释放锁完毕-------------"); System.out.println(); } } }
创建子类实现上面的 抽象方法
/** 查看节点:ls / * 删除节点:delete /node * 节点粗分为临时节点和持久性节点 * 创建临时节点: create -e /node value * 创建持久性节点create /node value */ public class ZkDistrbuteLock extends ZkAbstractLock { boolean tryLock() { try { //创建临时节点 zkClient.createEphemeral(lockPath); return true; } catch (Exception e) { return false; } } void waitLock() { //zk数据监听器 IZkDataListener iZkDataListener = new IZkDataListener() { // 节点被删除 public void handleDataDeleted(String arg0) throws Exception { if (countDownLatch != null) { countDownLatch.countDown(); // 计数器为0的情况,await 后面的继续执行 } } // 节点被修改 public void handleDataChange(String arg0, Object arg1) throws Exception { } }; // 加监听事件 zkClient.subscribeDataChanges(lockPath, iZkDataListener); if (zkClient.exists(lockPath)) { countDownLatch = new CountDownLatch(1); try { countDownLatch.await(); //等待时候 就不往下走了 当为0 时候 后面的继续执行 } catch (InterruptedException e) { e.printStackTrace(); } } //解除监听事件 zkClient.unsubscribeDataChanges(lockPath, iZkDataListener); } }
获取订单号业务层1(使用juc lock锁机制)
public class OrderService1 { private OrderNumGeneratorUtil orderNumGeneratorUtil=new OrderNumGeneratorUtil(); public void getNumber1(){ String str= orderNumGeneratorUtil.getNumber(); System.out.println(str); } //使用juc lock锁机制 private Lock lock=new ReentrantLock(); public void getNumber2(){ lock.lock(); try{ String str= orderNumGeneratorUtil.getNumber(); System.out.println(str); }finally { lock.unlock(); } } }
获取订单号业务层2(使用zk分布锁机制)
/** * 使用zk分布锁机制 */ public class OrderService2 { private OrderNumGeneratorUtil orderNumGeneratorUtil=new OrderNumGeneratorUtil(); private ZkDistrbuteLock zkLock=new ZkDistrbuteLock(); //获取订单号 public void getNumber(){ //zk加锁 zkLock.lock(); try{ String str= orderNumGeneratorUtil.getNumber(); System.out.println(str); }finally { //zk释放锁 zkLock.unLock(); } } }
测试方法
/** * 多线程下获取订单测试类 */ public class ClientTest { //多线程未加锁生成订单号 public static void test1(){ OrderService1 orderService1=new OrderService1(); for(int i=1;i<=500;i++){ new Thread(()-> { orderService1.getNumber1(); }).start(); } } //lock加锁 public static void test2(){ OrderService1 orderService1=new OrderService1(); for(int i=1;i<=500;i++){ new Thread(()-> { orderService1.getNumber2(); }).start(); } } //多jvm下 lock加锁 public static void test3(){ for(int i=1;i<=500;i++){ new Thread(()-> { new OrderService1().getNumber2(); }).start(); } } //多jvm下 zookeeper加锁 public static void test4(){ for(int i=1;i<=500;i++){ new Thread(()-> { new OrderService2().getNumber(); }).start(); } } public static void main(String[] args) { //test1(); //test2(); //test3(); test4(); } }
运行结果: