一.为什么需要分布式锁
场景描述:小型电商网站,下单,生产有一定业务含义的唯一订单编号
如2019-01-12-23:00:00-01
OrderService(订单服务类) ---->OrderCodeGenerator(订单编号生成类)
package com.hong.order;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class OrderCodeGenerator {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss-");
//自增序列
private int sequence;
/**
* 订单编号生成,格式:yyyy-MM-dd-HH-mm-ss-sequence
* @return
*/
public String generateCode(){
LocalDateTime now = LocalDateTime.now();
return now.format(FORMATTER) + (++sequence);
}
}
package com.hong.order;
public class OrderService {
private OrderCodeGenerator org = new OrderCodeGenerator();
public void createOrder(){
String orderCode = org.generateCode();
System.out.println(Thread.currentThread().getName() + "====>" + orderCode);
//其他业务
}
}
package com.hong.order;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CurrentOrderTest {
/**
* 模拟多人同时下单
*
* @param args
*/
public static void main(String[] args) {
int currency = 50;
// 循环屏障
CyclicBarrier cb = new CyclicBarrier(currency);
OrderService orderService = new OrderService();
for (int i = 0; i < currency; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "----准备生成订单---");
try {
// 等待所有人准备好了,一起出发
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
orderService.createOrder();
}).start();
}
}
}
输出结果中会发现重复的订单号。
单体应用可以直接使用JDK中的锁控制并发.
package com.hong.order;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class OrderServiceWithLock {
private OrderCodeGenerator org = new OrderCodeGenerator();
/**
* 使用ReentrantLock,它只能锁住一个JVM进程内的共享资源,但分布式是跨JVM进程的
*/
private Lock lock = new ReentrantLock();
public void createOrder() {
String orderCode = null;
try {
lock.lock();
orderCode = org.generateCode();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "====>" + orderCode);
//其他业务
}
}
如果单台服务器无法撑起并发量…,怎么办?
必然上集群.
如何模拟验证上面的集群?
package com.hong.order;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CurrentOrderTest {
/**
* 模拟多人同时下单
*
* @param args
*/
public static void main(String[] args) {
int currency = 50;
// 循环屏障
CyclicBarrier cb = new CyclicBarrier(currency);
//OrderService orderService = new OrderService();
// OrderServiceWithLock orderService = new OrderServiceWithLock();
for (int i = 0; i < currency; i++) {
new Thread(() -> {
// 模拟集群环境,OrderServiceWithLock部署在不同的机器上
OrderServiceWithLock orderService = new OrderServiceWithLock();
System.out.println(Thread.currentThread().getName() + "----准备生成订单---");
try {
// 等待所有人准备好了,一起出发
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
orderService.createOrder();
}).start();
}
}
}
此时控制台输出的序列号尾数全部为1: Thread-4====>2019-01-13-09-53-05-1
二.分布式场景模拟
将订单编号生成独立共享的服务.
package com.hong.order;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class OrderServiceWithLock {
//private OrderCodeGenerator org = new OrderCodeGenerator();
// 模拟OrderCodeGenerator为共享资源
private static OrderCodeGenerator org = new OrderCodeGenerator();
...
此时,虽然有锁,但各tomcat中使用的是各自的锁,订单编号还是存在重复.
三.分布式锁登场
在分布式环境下协同共享资源的使用.
分布式锁的实现方式有哪些?
1.锁具有什么特点?
排他性:同一时刻,只有一个线程能获取到
阻塞性:其他未抢到锁的线程阻塞,直到锁释放出来,再抢.
可重入性:线程获得锁后,后续是否可重复获取该锁.
2.我们掌握的计算机技术中,有哪些能提供排他性?
文件系统:同一个目录下不能存在重名的文件或目录
数据库:主键唯一约束 for update
缓存Redis setnx
ZooKeeper: 类似文件系统
3.常用分布式锁实现技术
- 基于数据库的实现
性能较差,容易出现单点故障
锁没有失效时间,容易死锁 - 基于缓存实现
实现复杂
存在死锁(或短时间死锁)的可能 - 基于ZooKeeper实现
实现相对简单
可靠性高
性能较好
四.基于Zookkeeper实现分布式锁
同一个znode下,节点名称是唯一的.删除一个节点,要先删除它下面的子节点.
Zookeeper节点类型
用持久节点可以吗?
不可以,如果持有锁的进程挂了,节点的不到删除,会造成死锁.
所以用临时节点.
Zookeeper分布式锁版本一
package com.hong.zoo;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class ZkDistributeLock implements Lock{
private String lockPath;
private ZkClient client;
public ZkDistributeLock(String lockPath) {
this.lockPath = lockPath;
client = new ZkClient("localhost:2181");
client.setZkSerializer(new MyZkSerializer());
}
/**
* 如果获取不到锁,则阻塞等待
*/
@Override
public void lock() {
if (!tryLock()){
// 没获得锁,阻塞自己
waitForLock();
lock();
}
}
/**
* 注册节点的watcher,阻塞等待
*/
private void waitForLock() {
CountDownLatch cdl = new CountDownLatch(1);
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String lockPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String lockPath) throws Exception {
System.out.println("===收到节点被删除了===");
cdl.countDown();
}
};
client.subscribeDataChanges(lockPath,listener);
// 阻塞自己
if (this.client.exists(lockPath)){
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 取消注册
client.unsubscribeDataChanges(lockPath,listener);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
/**
* 非阻塞获取锁
* @return
*/
@Override
public boolean tryLock() {
try {
client.createEphemeral(lockPath);
} catch (ZkNodeExistsException e) {
return false;
}
return true;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
client.delete(lockPath);
}
@Override
public Condition newCondition() {
return null;
}
}
控制台打印如下:
…
=收到节点被删除了=
=收到节点被删除了=
=收到节点被删除了=
=收到节点被删除了=
=收到节点被删除了=
=收到节点被删除了=
=收到节点被删除了=
Thread-30====>2019-01-13-15-44-00-50
一个节点抢占到锁,zookeeper会发出通知告诉其他等待获取锁的线程,这会造成"惊群效应".
"惊群效应"在集群规模较大的环境中带来的危害:
巨大的服务器性能损耗;
网络冲击;
可能造成宕机.
Zookeeper分布式锁版本二
改进后的实现数据结构与逻辑.
package com.hong.zoo;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 改进后的Zookeeper分布式锁
*/
public class ZkDistributeImproveLock implements Lock{
/**
* 利用临时顺序节点来实现分布式锁
* 获取锁:取排队号(创建自己的临时顺序节点),然后判断自己是否是最小号,如是,则获得锁;
* 不是,则注册前一节点的watcher,阻塞等待
* 释放锁:删除自己创建的临时顺序节点
*/
private String lockPath; //现在的lockPath变成了父节点,且是个持久化节点
private ZkClient client;
private String currentPath;
private String beforePath;
public ZkDistributeImproveLock(String lockPath) {
this.lockPath = lockPath;
client = new ZkClient("localhost:2181");
client.setZkSerializer(new MyZkSerializer());
if (!this.client.exists(lockPath)){
try {
this.client.createPersistent(lockPath);
} catch (RuntimeException e) {
e.printStackTrace();
}
}
}
/**
* 如果获取不到锁,则阻塞等待
*/
@Override
public void lock() {
if (!tryLock()){
// 没获得锁,阻塞自己
waitForLock();
lock();
}
}
/**
* 注册节点的watcher,阻塞等待
*/
private void waitForLock() {
CountDownLatch cdl = new CountDownLatch(1);
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String lockPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String lockPath) throws Exception {
System.out.println("===收到节点被删除了===");
cdl.countDown();
}
};
client.subscribeDataChanges(this.beforePath,listener);
// 使用CountDownLatch cdl = new CountDownLatch(1);实现阻塞自己
if (this.client.exists(this.beforePath)){
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 醒来后,取消注册
client.unsubscribeDataChanges(this.beforePath,listener);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
/**
* 非阻塞获取锁
* @return
*/
@Override
public boolean tryLock() {
if (this.currentPath == null){
currentPath = this.client.createEphemeralSequential(lockPath + "/" , "number");
}
// 获得所有的子节点
List<String> children = this.client.getChildren(lockPath);
Collections.sort(children);
//判断当前节点是否是最小的
if (currentPath.equals(lockPath + "/" + children.get(0))){
return true;
}else {
// 取到前一个,得到字节的索引号
int curIndex = children.indexOf(currentPath.substring(lockPath.length() + 1));
beforePath = lockPath + "/" + children.get(curIndex-1);
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
client.delete(this.currentPath);
}
@Override
public Condition newCondition() {
return null;
}
}
五.大型互联网应用架构
需要掌握以下核心技术:
- 高并发集群技术
- 高性能并发编程技术
- 分布式微服务技术
- 大型分布式缓存技术
- 消息中间件技术
- 搜索引擎技术
- MySQL集群技术
- NoSql技术