服务注册与发现
场景
一个产品服务productService,一个订单服务orderService。下订单的时候要获取产品信息。
代码示例
项目总结构
productService
结构:
1、ProductApp
package cn.enjoy.product;
import cn.enjoy.product.listener.InitListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class ProductApp {
public static void main(String[] args) {
SpringApplication.run(ProductApp.class,args);
}
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean();
servletListenerRegistrationBean.setListener(new InitListener());
return servletListenerRegistrationBean;
}
}
2、InitListener
package cn.enjoy.product.listener;
import cn.enjoy.product.zk.ServiceRegister;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.net.InetAddress;
public class InitListener implements ServletContextListener {
@Value("${server.port}")
private int port;
@Override
public void contextInitialized(ServletContextEvent sce) {
WebApplicationContextUtils.getRequiredWebApplicationContext(sce.getServletContext()).getAutowireCapableBeanFactory().autowireBean(this);
try {
String hostAddress = InetAddress.getLocalHost().getHostAddress();
ServiceRegister.register(hostAddress,port);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
3、ServiceRegister
package cn.enjoy.product.zk;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
public class ServiceRegister {
private static final String BASE_SERVICES = "/services";
private static final String SERVICE_NAME="/products";
public static void register(String address,int port) {
try {
//产品服务最终会注册到的地址
String path = BASE_SERVICES + SERVICE_NAME;
ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181",5000,(watchedEvent)->{});
Stat exists = zooKeeper.exists(BASE_SERVICES + SERVICE_NAME, false);
//先判断服务根路径是否存在
if(exists==null) {
zooKeeper.create(path,"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//拼接ip和端口
String server_path = address+":"+port;
//注册的类型,EPHEMERAL_SEQUENTIAL临时并且带序号。当客户端与服务端的连接关闭时,临时节点会自动删除。当服务挂了,节点应该删除
zooKeeper.create(BASE_SERVICES + SERVICE_NAME+"/child",server_path.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4、Product
package cn.enjoy.product.pojo;
public class Product {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product(String id, String name) {
this.id = id;
this.name = name;
}
public Product() {
}
}
5、ProductController
package cn.enjoy.product.controller;
import cn.enjoy.product.pojo.Product;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/product")
public class ProductController {
@RequestMapping("/getProduct/{id}")
public Object getProduct(HttpServletRequest request, @PathVariable("id") String id) {
return new Product(id,"name:"+request.getLocalPort());
}
}
6、application.properties
server.port=8081
orderService
结构:
1、OrderApp
package cn.enjoy.order;
import cn.enjoy.order.listener.InitListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class,args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean();
servletListenerRegistrationBean.setListener(new InitListener());
return servletListenerRegistrationBean;
}
}
2、InitListener
package cn.enjoy.order.listener;
import cn.enjoy.order.utils.LoadBalance;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.ArrayList;
import java.util.List;
public class InitListener implements ServletContextListener {
private static final String BASE_SERVICES = "/services";
private static final String SERVICE_NAME="/products";
private ZooKeeper zooKeeper;
@Override
public void contextInitialized(ServletContextEvent sce) {
try {
//连接上zk,获得列表信息
zooKeeper = new ZooKeeper("localhost:2181",5000,(watchedEvent)->{
//当有节点变更的时候通知到orderService
if(watchedEvent.getType() == Watcher.Event.EventType.NodeChildrenChanged && watchedEvent.getPath().equals(BASE_SERVICES+SERVICE_NAME)) {
updateServiceList();
}
});
//第一次连接的时候要获得列表
updateServiceList();
} catch (Exception e) {
e.printStackTrace();
}
}
private void updateServiceList() {
try{
List<String> children = zooKeeper.getChildren(BASE_SERVICES + SERVICE_NAME, true);
List<String> newServerList = new ArrayList<String>();
for(String subNode:children) {
byte[] data = zooKeeper.getData(BASE_SERVICES + SERVICE_NAME + "/" + subNode, false, null);
String host = new String(data, "utf-8");
System.out.println("host:"+host);
newServerList.add(host);
}
LoadBalance.SERVICE_LIST = newServerList;
}catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
3、LoadBalance
package cn.enjoy.order.utils;
import java.util.List;
public abstract class LoadBalance {
public volatile static List<String> SERVICE_LIST;
public abstract String choseServiceHost();
}
4、RamdomLoadBalance
package cn.enjoy.order.utils;
import org.springframework.util.CollectionUtils;
import java.util.Random;
public class RamdomLoadBalance extends LoadBalance {
@Override
public String choseServiceHost() {
String result = "";
if(!CollectionUtils.isEmpty(SERVICE_LIST)) {
int index = new Random().nextInt(SERVICE_LIST.size());
result = SERVICE_LIST.get(index);
}
return result ;
}
}
5、Product
package cn.enjoy.order.pojo;
public class Product {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product(String id, String name) {
this.id = id;
this.name = name;
}
public Product() {
}
}
6、Order
package cn.enjoy.order.pojo;
public class Order {
private String id;
private String name;
private Product product;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
public Order(String id, String name, Product product) {
this.id = id;
this.name = name;
this.product = product;
}
public Order() {
}
}
7、OrderController
package cn.enjoy.order.controller;
import cn.enjoy.order.pojo.Order;
import cn.enjoy.order.pojo.Product;
import cn.enjoy.order.utils.LoadBalance;
import cn.enjoy.order.utils.RamdomLoadBalance;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RequestMapping("/order")
@RestController
public class OrderController {
@Resource
private RestTemplate restTemplate;
private LoadBalance loadBalance = new RamdomLoadBalance();
@RequestMapping("/getOrder/{id}")
public Object getOrder(@PathVariable("id") String id ) {
Product product = restTemplate.getForObject("http://"+loadBalance.choseServiceHost()+"/product/getProduct/1", Product.class);
return new Order(id,"orderName",product);
}
}
zookeeper分布式锁
基于同名节点的分布式锁
原理图
说明:所有客户端尝试创建临时节点,创建成功则说明成功获取锁,若创建失败,则说明创建失败,然后一直自旋,等待创建成功的client删除临时节点,再尝试获取锁资源。
代码示例
1、Lock
package com.ty; public interface Lock { //获取到锁的资源 void getLock(); //释放锁的资源 void unLock(); }
2、AbstractLock
package com.ty; public abstract class AbstractLock implements Lock { public void getLock() { if(tryLock()) { System.out.println("##获取lock锁的资源###"); unLock(); }else { //未获取到锁资源,等待 waitLock(); //继续准备获取锁资源 getLock(); } } public abstract boolean tryLock(); public abstract void waitLock(); }
3、ZookeeperAbstractLock
package com.ty; import org.I0Itec.zkclient.ZkClient; public abstract class ZookeeperAbstractLock extends AbstractLock { //zk连接地址 private static final String CONNECTSTRING = "127.0.0.1:2181"; //创建zk连接 protected ZkClient zkClient = new ZkClient(CONNECTSTRING); protected static final String PATH = "/lock"; protected static final String PATH2 = "/lock2"; }
4、ZookeeperDistributeLock
package com.ty; import org.I0Itec.zkclient.IZkDataListener; import java.util.concurrent.CountDownLatch; public class ZookeeperDistributeLock extends ZookeeperAbstractLock { private CountDownLatch countDownLatch = null; public boolean tryLock() { try { zkClient.createEphemeral(PATH); return true; }catch (Exception ex) { return false; } } public void waitLock() { //如果节点存在 if(zkClient.exists(PATH)) { countDownLatch = new CountDownLatch(1); IZkDataListener izkDataListener = new IZkDataListener() { public void handleDataChange(String s, Object o) throws Exception { //唤醒被等待的线程 if(countDownLatch != null) { countDownLatch.countDown(); } } public void handleDataDeleted(String s) throws Exception { } }; //注册事件 zkClient.subscribeDataChanges(PATH, izkDataListener); try{ //等待,一直等到接收到事件通知 countDownLatch.await(); }catch (Exception ex) { } //删除监听 zkClient.subscribeDataChanges(PATH, izkDataListener); } } public void unLock() { //释放锁 if(zkClient != null) { zkClient.delete(PATH); zkClient.close(); System.out.println("释放锁资源......"); } } }
优缺点
优点:实现简单
缺点:性能不好,当一个client释放锁之后,有很多client竞争锁资源,出现羊群效应。
高性能分布式锁
原理图
节点图
代码示例
Lock等代码与上面一种都是相同的,区别在于ZookeeperDistributeLock类上
1、HignPerformanceZookeeperDistributeLock
package com.ty; import org.I0Itec.zkclient.IZkDataListener; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; public class HignPerformanceZookeeperDistributeLock extends ZookeeperAbstractLock { private CountDownLatch countDownLatch = null; //当前请求的节点前的一个节点 private String beforePath; //当前请求的节点 private String currentPath; public HignPerformanceZookeeperDistributeLock() { if(!this.zkClient.exists(PATH2)) { this.zkClient.createPersistent(PATH2); } } public boolean tryLock() { //如果currentPath为空则为第一次尝试加锁,第一次加锁赋值currentpath if(currentPath == null || currentPath.length() == 0) { //创建一个临时顺序节点 currentPath = this.zkClient.createEphemeralSequential(PATH2 + "/", "lock"); } //获取所有临时节点并排序,临时节点名称为自增长的字符串,如00000001 List<String> children = this.zkClient.getChildren(PATH2); Collections.sort(children); //如果当前节点在所有节点中排名第一则获取锁成功 if(currentPath.equals(PATH2 + "/" + children.get(0))) { return true; }else { //如果当前节点在所有节点排名中不是排名第一,则获取前面的节点名称,并赋值给beforePath int wz = Collections.binarySearch(children, currentPath.substring(7)); beforePath = PATH2 + "/" + children.get(wz - 1); } return false; } public void waitLock() { if(this.zkClient.exists(beforePath)) { countDownLatch = new CountDownLatch(1); IZkDataListener listener = new IZkDataListener() { public void handleDataChange(String s, Object o) throws Exception { if(countDownLatch != null) { countDownLatch.countDown(); } } public void handleDataDeleted(String s) throws Exception { } }; //给排在前面的节点增加数据删除的watcher,本质是启动另外一个线程去监听前置节点 this.zkClient.subscribeDataChanges(beforePath, listener); try { countDownLatch.await(); }catch(Exception ex) { ex.printStackTrace(); } this.zkClient.unsubscribeDataChanges(beforePath, listener); } } public void unLock() { //删除当前临时节点 zkClient.delete(currentPath); zkClient.close(); } }