数据发布订阅/配置中心
实现配置信息的集中式管理和数据的动态更新,实现配置中心有两种模式:push(推送)、pull(主动拉取,长轮询)。
zookeeper采用的是推拉相结合的方式,客户端向服务器端注册自己需要关注的节点。一旦节点数据发生变化,那么服务器端就会向客户端发送watcher事件通知。客户端收到通知后,主动到服务器端获取更新后的数据。其特点如下:
- 数据量比较小
- 数据内容在运行时会发生动态变更
- 集群中的各个机器共享配置
Zookeeper配置管理
程序总是需要配置的,如果程序分散部署在多台服务器上,要逐个改变配置就变得很麻烦。而如果把这些配置全部放到zookeeper上去,保存在zookeeper的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到zookeeper的通知,然后从zookeeper获取新的配置信息应用到系统中。
实现分布式锁的几种常用方式
1. 数据库实现思路
在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
2. Redis实现思路
主要用到的命令如下:
(1)SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
(3)delete key:删除key
实现思想:
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
3. zookeeper实现思路及代码
ZooKeeper的内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
Tips:MySQL中的共享锁与排他锁
共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据,对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
用zookeeper的Java API实现分布式共享锁:
public class DistributeLock {
private static final String ROOT_LOCKS = "/LOCKS";//根节点
private ZooKeeper zooKeeper;
private int sessionTimeout; //会话超时时间
private String lockID; //记录锁节点id
private final static byte[] data = {1, 2}; //节点的数据
private CountDownLatch countDownLatch = new CountDownLatch(1);
public DistributeLock() throws IOException, InterruptedException {
this.zooKeeper = ZookeeperClient.getInstance();
this.sessionTimeout = ZookeeperClient.getSessionTimeout();
}
//获取锁的方法
public boolean lock(){
try {
//LOCKS/00000001
lockID = zooKeeper.create(ROOT_LOCKS+"/", data, ZooDefs.Ids.
OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(Thread.currentThread().getName() + "->成功创建了lock节点[" + lockID + "],开始去竞争锁");
//获取根节点下的所有子节点
List<String> childrenNodes = zooKeeper.getChildren(ROOT_LOCKS, true);
//排序,从小到大
SortedSet<String> sortedSet = new TreeSet<String>();
for(String children : childrenNodes){
sortedSet.add(ROOT_LOCKS + "/" + children);
}
String first = sortedSet.first(); //拿到最小的节点
if(lockID.equals(first)){
//表示当前就是最小的节点
System.out.println(Thread.currentThread().getName() + "->成功获得锁,lock节点为:[" + lockID + "]");
return true;
}
//获取所有比当前lockID小的节点集合
SortedSet<String> lessThanLockId = sortedSet.headSet(lockID);
if(!lessThanLockId.isEmpty()){
//拿到比当前lockID小的上一节点
String prevLockID = lessThanLockId.last();
//监听前一个节点是否被释放,然后设置监听的过期时间,如果监听超时则放弃获取锁,若节点被释放则立刻去获取锁
zooKeeper.exists(prevLockID, new LockWatcher(countDownLatch));
boolean flag = countDownLatch.await(sessionTimeout, TimeUnit.MILLISECONDS);
if(!flag)
return false;
System.out.println(Thread.currentThread().getName() + " 成功获取锁:[" + lockID + "]");
}
return true;
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
public boolean unlock(){
System.out.println(Thread.currentThread().getName() + "->开始释放锁:[" + lockID + "]");
try {
zooKeeper.delete(lockID, -1);
System.out.println("节点[" + lockID + "]成功被删除");
return true;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
return false;
}
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(10);
Random random = new Random();
for(int i=0; i<10; i++){
new Thread(()->{
DistributeLock lock = null;
try {
lock = new DistributeLock();
//使用闭锁,当10个线程都countDown()后,释放闭锁,使10个线程同时去争夺锁
latch.countDown();
latch.await();
lock.lock();
Thread.sleep(random.nextInt(500));
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(lock != null){
lock.unlock();
}
}
}).start();
}
}
}
/**
* @author 全恒
* 创建会话
*/
public class ZookeeperClient {
private final static String CONNECTSTRING = "192.168.123.38:2181,192.168.123.55:2181," +
"192.168.123.45:2181,192.168.123.174:2181";
private static int sessionTimeout = 5000;
//获取连接
public static ZooKeeper getInstance() throws IOException, InterruptedException {
final CountDownLatch conectStatus = new CountDownLatch(1);
ZooKeeper zooKeeper = new ZooKeeper(CONNECTSTRING, sessionTimeout, new Watcher() {
public void process(WatchedEvent event) {
if(event.getState() == Event.KeeperState.SyncConnected){
conectStatus.countDown();
}
}
});
conectStatus.await();
return zooKeeper;
}
public static int getSessionTimeout() {
return sessionTimeout;
}
}
public class LockWatcher implements Watcher{
private CountDownLatch latch;
public LockWatcher(CountDownLatch latch) {
this.latch = latch;
}
public void process(WatchedEvent event) {
//如果节点被删除,则释放 闭锁
if(event.getType() == Event.EventType.NodeDeleted){
latch.countDown();
}
}
}
Zookeeper实现master选举
随着分布式系统的流行,现在许多服务都需要24小时工作,如果机器挂了的话,我们希望能有其他机器顶替他继续工作。这种问题通常采用master-slave模式,即是正常情况下主机提供服务,备用机器监听主机状态,当主机发生异常时,会选取一个备用机器代替主机器提供服务,这个选举过程即是master选举。
由于zookeeper能保证同一时刻只能具有一个主节点,即使有网络问题,zookeeper的集群也完全可以解决,而且zookeeper的通知机制,客户端可以监听节点的变化,所以可以考虑采用zookeeper实现master选举。其实现原理图如下:
上图中,左侧是zookeeper节点,master节点保存当前master的机器,server节点保存可用的备用机器,每个服务器在创建的时候会进行选举,即每个服务器同时去创建master节点,只有一个机器能创建成功,创建成功的机器则被选为master,其他机器会同时监听master节点的变化,当主机器挂了时,master节点会被删除,其他机器会重新进行master选举。
用zkclient实现master选举
/**
* @author 全恒
* 用户中心,用来模拟 争抢master节点的机器
*/
public class UserCenter implements Serializable{
private static final long serialVersionUID = -1776114173857775665L;
private int mc_id; //机器信息
private String mc_name;//机器名称
public int getMc_id() {
return mc_id;
}
public void setMc_id(int mc_id) {
this.mc_id = mc_id;
}
public String getMc_name() {
return mc_name;
}
public void setMc_name(String mc_name) {
this.mc_name = mc_name;
}
@Override
public String toString() {
return "UserCenter{" + "mc_id=" + mc_id + ", mc_name='" + mc_name + '\'' + '}';
}
}
/**
* @author 全恒
* 提供master选举的服务
*/
public class MasterSelector {
private ZkClient zkClient;
private final static String MASTER_PATH = "/master"; //需要争抢的master节点
private IZkDataListener dataListener; //注册节点内容变化
private UserCenter server; //其他服务器
private UserCenter master; //master节点
private boolean isRunning = false;
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
public MasterSelector(UserCenter server, ZkClient zkClient) {
System.out.println("[" + server + "] 去争抢master权限");
this.server = server;
this.zkClient = zkClient;
this.dataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
//节点如果被删除, 重新进行master选举
chooseMaster();
}
};
}
public void start(){
//开始选举,如果当前机器已经启动,则不进行任何处理
if(!isRunning){
isRunning = true;
//注册节点事件,使当前客户端机器监听master节点的删除动作
zkClient.subscribeDataChanges(MASTER_PATH, dataListener);
chooseMaster();
}
}
public void stop(){
//停止,如果当前机器已经停止,则不进行任何处理
if(isRunning){
isRunning = false;
//关闭定时器
scheduledExecutorService.shutdown();
//取消订阅
zkClient.unsubscribeDataChanges(MASTER_PATH, dataListener);
releaseMaster();
}
}
//具体选master的实现逻辑
private void chooseMaster(){
if(!isRunning){
System.out.println("当前服务没有启动");
return ;
}
try {
zkClient.createEphemeral(MASTER_PATH, server);
master=server; //把server节点赋值给master
System.out.println(master + "->我现在已经是master,你们要听我的");
//使用定时器模拟 主服务器出现故障,每2秒释放一次
scheduledExecutorService.schedule(()->{
releaseMaster();//释放锁(模拟故障的发生)
}, 2, TimeUnit.SECONDS);
} catch (ZkNodeExistsException e){
//表示master已经存在
UserCenter userCenter = zkClient.readData(MASTER_PATH, true);
if(userCenter == null) {
System.out.println("启动操作:");
chooseMaster(); //再次获取master
} else {
master = userCenter;
}
}
}
//释放锁(模拟故障的发生)
private void releaseMaster(){
//判断当前是不是master,只有master才需要释放
if(checkMaster()){
zkClient.delete(MASTER_PATH); //删除
}
}
//判断当前的server是不是master
private boolean checkMaster(){
UserCenter userCenter = zkClient.readData(MASTER_PATH);
if(userCenter.getMc_name().equals(server.getMc_name())){
master = userCenter;
return true;
}
return false;
}
}
/**
* @author 全恒
* 模拟多个机器抢夺master角色
*/
public class MasterChooseTest {
private final static String CONNECTSTRING = "192.168.123.38:2181,192.168.123.55:2181," +
"192.168.123.45:2181,192.168.123.174:2181";
public static void main(String[] args) throws IOException {
List<MasterSelector> selectorLists = new ArrayList<>();
try {
for(int i=0; i<10; i++) {
ZkClient zkClient = new ZkClient(CONNECTSTRING, 5000,
5000, new SerializableSerializer());
UserCenter userCenter = new UserCenter();
userCenter.setMc_id(i);
userCenter.setMc_name("客户端:" + i);
MasterSelector selector = new MasterSelector(userCenter, zkClient);
selectorLists.add(selector);
selector.start();//触发选举操作
TimeUnit.SECONDS.sleep(1);//睡眠1秒
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
for(MasterSelector selector:selectorLists){
selector.stop();
}
}
}
}
用curator实现master选举
public class MasterSelector {
private final static String CONNECTSTRING = "192.168.123.38:2181,192.168.123.55:2181," +
"192.168.123.45:2181,192.168.123.174:2181";
private final static String MASTER_PATH = "/curator_master_path";
public static void main(String[] args) {
CuratorFramework curatorFramework = CuratorFrameworkFactory
.builder()
.connectString(CONNECTSTRING)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
@SuppressWarnings("resource")
LeaderSelector leaderSelector = new LeaderSelector(curatorFramework, MASTER_PATH, new LeaderSelectorListenerAdapter() {
@Override
public void takeLeadership(CuratorFramework client) throws Exception {
System.out.println("获得leader成功");
TimeUnit.SECONDS.sleep(2);
}
});
leaderSelector.autoRequeue();//自动争抢leader
leaderSelector.start();//开始选举
}
}