如今的企业级互联网大都存在“集群”,将一个子系统多实例化,经过负载均衡分摊用户的请求,这种集群式的部署,给企业级应用带来性能和效率上的提升,但是也带来了不少问题,如 高并发场景下多个线程同时访问、操作共享资源的时候,出现了数据不一致的现象,比如商品入库,出现了超卖,针对这种情况,业界普遍采用的是分布式锁加以解决本章开始将将探索分布式锁的相关要点。
分布式锁的概念
如果在传统的单体应用中,并发访问共享资源的场景不少见,往往解决方式就是同步互斥锁进行控制,比如synchronized,这是单独一个jvm的情况,但是分布式锁的跨JVM进程的资源共享,因此,引入了分布式锁。
锁机制
除了JDK提供的锁机制,我们先来介绍一下什么是共享资源,并且看一下不加锁会造成什么情况
以银行为业务,比如银行有500块,A取出100(多线程下),B存入100(多线程下)
package com.learn.boot.test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
//定义日志
private static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) {
// A取出100块循环10次
LocalMoney aLocalMoney = new LocalMoney(-100);
// B 存入100块循环10次
LocalMoney bLocalMoney = new LocalMoney(100);
ExecutorService executor = Executors.newFixedThreadPool(10) ;
executor.submit(aLocalMoney);
executor.submit(bLocalMoney);
}
}
/**
* 银行(共有资源)
*/
class LocalNumber {
/**
* 银行总数
*
*/
public static Integer amount = 500;
}
class LocalMoney implements Runnable {
private static final Logger log = LoggerFactory.getLogger(LocalMoney.class);
private Integer data;
LocalMoney(Integer data) {
this.data = data;
}
@Override
public void run() {
//通过传进来的金额(可正、可负)执行叠加操作
for (int i = 0; i< 10;i++) {
LocalNumber.amount = LocalNumber.amount + data;
//输出每次操作完账户的余额
log.info("B存入100块 此时账户余额为:{}", LocalNumber.amount);
}
}
}
多运行几次,会发现很多问题
发现一个问题,就是银行的余额居然多了100块,而且每次取的时候有可能造成共享资源没被扣减的问题。
主要问题在于
因为这里是循环对共享资源操作,一个线程还没有对amount操作完,下一个线程就进来把amount用了。
解决方案,上述代码加上锁synchronized
@Override
public void run() {
//通过传进来的金额(可正、可负)执行叠加操作
synchronized (LocalNumber.amount) {
for (int i = 0; i< 10;i++) {
LocalNumber.amount = LocalNumber.amount + data;
//输出每次操作完账户的余额
log.info("{}100块 此时账户余额为:{}",name, LocalNumber.amount);
}
}
}
最后无论你循环多少次,最终的结果都是一样的。事实上采用Synchronized是有缺陷的具体可以网上查找其他并发工具类是如何加锁的
分布式锁登场
上述情况,如果是多个实例访问,那么会显得力不从心,因为这种单体应用的锁只适合一个JVM操作下的。分布式锁就是为了解决这一情况,通过锁机制让多个服务的进程互斥地对共享资源进行访问,从而避免出现并发安全,数据不一致的问题。但是由于网络的不稳定性,如果分布式锁设计的不合理,那么将很有可能出现死锁的情况,因此为分布式锁,应该提出以下要求
基于上述几点要求,业界提供多了多种解决方案
基于数据库的乐观锁,,悲观锁,基于Redis的原子操作,基于Zookeeper的互斥排它锁,以及基于开源框架Redisson的分布式锁
基于数据库级别的乐观锁
主要通过查询,操作共享数据记录时带上一个标志字段(version),通过version来控制每次对数据记录执行的更新操作。
基于数据库级别的悲观锁
它主要在访问共享的数据记录时加上for update的关键字,表示该共享的数据记录已经被当前线程锁住了(行级别锁,表级别锁),只有当该线程操作完后并提交事务,才会释放锁,从而其他线程才能访问该数据记录。
基于Redis的原子操作
主要通过Redis提供的原子操作setnx和expire来实现,setnx表示只有key在redis中不存在时才设置成功,通常这个key需要设置为共享的资源有联系,用于间接的当做“锁”,并且expire才释放锁
基于Zookeeper的互斥排它锁
主要是通过Zookeepeer在指定的标志字符串(通常这个标志字符串需要设置为与共享资源有联系,既可以间接当锁)下维护一个临时有序的节点列表NodeList,并保证同一时刻并发线程访问共享资源时只能有一个最小序号的节点(代表获取锁的线程),该节点对应的线程即可执行访问共享资源的操作。
以上几种分布式锁的方式,在后面文章中会进行着重的原理分析以及代码实战,对于开源框架Redisson,其性能极佳,这里会在讲述Reddisson对其进行介绍。