所谓面试,其实就是在短时间内推销自己。5分技术,5分装逼。一定要把装逼的知识点在面试中表现出来!
1.分布式锁
1.1.什么是分布式锁
分布式锁其实可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性。 举个不太恰当的例子:假设共享的资源就是一个房子,里面有各种书,分布式系统就是要进屋看书的人,分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进,而且门只有一把钥匙。然后许多人要去看书,可以,排队,第一个人拿着钥匙把门打开进屋看书并且把门锁上,然后第二个人没有钥匙,那就等着,等第一个出来,然后你在拿着钥匙进去,然后就是以此类推!
1.2.项目中应用场景
场景:在下单木块,使用ZooKeeper完成分布式锁,保证在集群环境下,生成不重复的唯一订单号。
描述:在订单模块,需要生成订单编号。其实关于订单编号不重复,可以有多种方式来实现。
思路1:uuid、时间戳在集群下都会重复。
思路2:提前生成100万个订单号,存放在redis中,如果redis中还剩下10万个,再重新生成,解决高并发下订单号的幂等性。
思路3:使用雪花算法来生成不重复的唯一订单编号
问题:项目经理要求生成的订单号需要有语义,规则是: 年月日时分秒毫秒+订单序号,那么就意味着不能提前生成
解决办法:使用ZooKeeper实现分布式锁,完成订单编号的生成
1.4.实现思路
分布式锁使用ZooKeeper的临时节点+通知机制完成!
分布式锁使用zk,在zk上创建一个临时节点(有效期),使用临时节点作为锁,因为节点不允许重复,而且客户端断开临时节点就会自动删除。
如果能创建节点成功,生成订单号,如果创建节点失败,等待并注册对临时节点的监听。临时节点zk关闭,释放锁,那么就有其他节点收到通知,其他节点再去竞争的创建临时节点,重新生成订单号。
1.5.代码实现
代码说不出来不要紧,重点的是实现的思路!!
//生成订单号
public class OrderNumGenerator {
private static int count = 0;
// 生成订单号
public String getOrderNumber() {
SimpleDateFormat smt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
return smt.format(new Date()) + "-" + ++count;
}
}
// s#####lock接口 ######
public interface Lock {
// 获取锁
public void getLock();
// 释放锁
public void unLock();
}
// #####ZookeeperAbstractLock抽象类接口 ######
public abstract class ZookeeperAbstractLock implements Lock {
private static final String CONNECT_ADDRES = "192.168.59.136:2181";
protected ZkClient zkClient = new ZkClient(CONNECT_ADDRES);
protected String PATH = "/lock";
public void getLock() {
// 如果当前节点已经存在,则等待
if (tryLock()) {
System.out.println("获取到锁 get");
} else {
// 等待
waitLock();
// 重新获取锁
getLock();
}
}
protected abstract void waitLock();
protected abstract boolean tryLock();
public void unLock() {
if (zkClient != null) {
zkClient.close();
}
System.out.println("已经释放锁...");
}
}
// #####ZookeeperAbstractLock抽象类接口 ######
//实现锁
public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock {
private CountDownLatch countDownLatch = new CountDownLatch(1);
@Override
protected boolean tryLock() {
try {
zkClient.createEphemeral(PATH);
// 创建成功
return true;
} catch (Exception e) {
// 创建失败
return false;
}
}
@Override
protected void waitLock() {
try {
//监听Zk节点删除事件
IZkDataListener iZkDataListener = new IZkDataListener() {
public void handleDataDeleted(String path) throws Exception {
// 唤醒等待线程, 继续往下走.
if (countDownLatch != null) {
countDownLatch.countDown();
System.out.println("删除节点");
}
}
public void handleDataChange(String path, Object data) throws Exception {
}
};
// 注册到zk监听中
zkClient.subscribeDataChanges(PATH, iZkDataListener);
if (zkClient.exists(PATH)) {
countDownLatch = new CountDownLatch(1);
// 等待
countDownLatch.await();
}
// 删除事件通知
zkClient.unsubscribeDataChanges(PATH, iZkDataListener);
} catch (Exception e) {
// TODO: handle exception
}
}
}
//#####订单业务逻辑######
public class OrderService implements Runnable {
private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
private Lock lock = new ZookeeperDistrbuteLock();
public void run() {
getNumber();
}
public void getNumber() {
lock.getLock();
String orderNumber = orderNumGenerator.getOrderNumber();
System.out.println("获取订单号:" + orderNumber);
lock.unLock();
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new OrderService()).start();
}
}
}
2.分布式事务
2.1.什么是分布事务
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
2.2.项目中应用场景
场景:在电商项目中一个客户在下订单后需要把订单数据进行持久化,落库操作。同时也需要对商品进行减库存操作,因为我们使用的是SpringCloud微服务架构,订单服务处理的是订单数据库,商品处理的是商品数据库,所以需要保证下单逻辑整体上数据一致性,需要用到分布式事务!
2.3.解决方案(装逼理论)
以下是理论的装逼点 (装逼理论)
2.3.1.两段提交(2PC)
说到2PC就不得不聊数据库分布式事务中的 XA Transactions。
XA协议中分为两阶段:
第一阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.
第二阶段:事务协调器要求每个数据库提交数据,或者回滚数据。
优点:
尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。
缺点:
**单点问题:**事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
总的来说,XA协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。
2.3.2.本地消息表
本地消息表这个方案最初是ebay提出的 ebay的完整方案https://queue.acm.org/detail.cfm?id=1394128。
此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。
对于本地消息队列来说核心是把大事务转变为小事务。用100元去买一瓶水的举例子。
1.当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和写入减去水的库存到本地消息表放入同一个事务(依靠数据库本地事务保证一致性。
2.这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫他减去水的库存,到达商品服务器之后这个时候得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。
3.商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器本地消息表进行状态更新。
4.针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的,如果已经接收,在判断是否执行,如果执行在马上又进行通知事务,如果未执行,需要重新执行需要由业务保证幂等,也就是不会多扣一瓶水。
本地消息队列是BASE理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等
2.4.我们项目中的解决方式
2.4.1 使用TX-LCN分布式事务框架
最近公司使用分布式框架搭建自己的服务(springCloud),就会遇到各位大神都会遇到的神级烦问题:分布式事务问题,近期研究了一下lcn框架的源码,觉得很不错,于是果断选用lcn控制分布式事务。
1.完全开源,可以在官方的基础上做自己的各种修改
2.不仅支持springCloud,而且支持dubbo
3.数据库方面,支持mybatis,jdbc等
正如lcn官网所说: LCN并不生产事务,LCN只是本地事务的协调者
官网(http://www.txlcn.org)
2.4.2 LCN原理
LCN分布式事务框架其本身并不创建事务,而是基于对本地事务的协调从而达到事务一致性的效果。
LCN5.0.2有3种模式,分别是LCN模式,TCC模式,TXC模式
LCN模式:
LCN模式是通过代理Connection的方式实现对本地事务的操作,然后在由TxManager统一协调控制事务。当本地事务提交回滚或者关闭连接时将会执行假操作,该代理的连接将由LCN连接池管理。
该模式的特点:
- 该模式对代码的嵌入性为低。
- 该模式仅限于本地存在连接对象且可通过连接对象控制事务的模块。
- 该模式下的事务提交与回滚是由本地事务方控制,对于数据一致性上有较高的保障。
- 该模式缺陷在于代理的连接需要随事务发起方一共释放连接,增加了连接占用的时间。
核心步骤:
1.创建事务组
是指在事务发起方开始执行业务代码之前先调用TxManager创建事务组对象,然后拿到事务标示GroupId的过程。
2.添加事务组
添加事务组是指参与方在执行完业务方法以后,将该模块的事务信息添加通知给TxManager的操作。
3.关闭事务组
是指在发起方执行完业务代码以后,将发起方执行结果状态通知给TxManager的动作。当执行完关闭事务组的方法以后,TxManager将根据事务组信息来通知相应的参与模块提交或回滚事务。
事务控制原理
LCN事务控制原理是由事务模块TxClient下的代理连接池与TxManager的协调配合完成的事务协调控制。
TxClient的代理连接池实现了javax.sql.DataSource接口,并重写了close方法,事务模块在提交关闭以后TxClient连接池将执行"假关闭"操作,等待TxManager协调完成事务以后在关闭连接。
2.4.2 使用TX-LCN搭建步骤
代码步骤答不上来就算了,但是理论的一定要说! 面试官会对你刮目相看的!
2.4.2.1.下载和配置LCN
- 下载LCN的相关开发包
- 把LCN导入到项目中
- 配置LCN
#eureka 地址 配置自己的eureka地址
eureka.client.service-url.defaultZone=http://127.0.0.1:8000/eureka
eureka.instance.prefer-ip-address=true
#redis主机地址
spring.redis.host=127.0.0.1
#redis主机端口
spring.redis.port=6379
- 启动事务协调器,注册到注册中心
- 分布式事务管理器WEB监控
2.4.2.2.准备和配置事务工程
- 准备一个微服务的工程—Orderservice
连接分布式事务协调器
#txmanager地址 事务协调器
tm.manager.url=http://127.0.0.1:8899/tx/manager/
- 和分布式协调器通信
/**
* TxClient和分布式事务协器通讯
*/
@Service
public class TxManagerTxUrlServiceImpl implements TxManagerTxUrlService {
@Value("${tm.manager.url}")
private String url;
@Override
public String getTxUrl() {
System.out.println("load tm.manager.url ");
return url;
}
}
- 注入数据库连接池
@Autowired
private Environment env;
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(env.getProperty("spring.datasource.url"));
dataSource.setUsername(env.getProperty("spring.datasource.username"));//用户名
dataSource.setPassword(env.getProperty("spring.datasource.password"));//密码
dataSource.setInitialSize(2);
dataSource.setMaxActive(20);
dataSource.setMinIdle(0);
dataSource.setMaxWait(50000);
dataSource.setValidationQuery("SELECT 1");
dataSource.setTestOnBorrow(false);
dataSource.setTestWhileIdle(true);
dataSource.setPoolPreparedStatements(false);
return dataSource;
}
-
准备另外一个微服务的工程—Goodsservice
步骤和上述一样! -
准备消费者工程
连接分布式事务协调器
#txmanager地址 事务协调器
tm.manager.url=http://127.0.0.1:8899/tx/manager/
- 分布式协调器通信
/**
* TxClient和分布式事务协器通讯
*/
@Service
public class TxManagerTxUrlServiceImpl implements TxManagerTxUrlService {
@Value("${tm.manager.url}")
private String url;
@Override
public String getTxUrl() {
System.out.println("load tm.manager.url ");
return url;
}
}
- 配置代码访问
@Service
public class TxManagerHttpRequestServiceImpl implements TxManagerHttpRequestService {
@Override
public String httpGet(String url) {
System.out.println("httpGet-start");
String res = HttpUtils.get(url);
System.out.println("httpGet-end");
return res;
}
@Override
public String httpPost(String url, String params) {
System.out.println("httpPost-start");
String res = HttpUtils.post(url,params);
System.out.println("httpPost-end");
return res;
}
}
- 调用代码访问,事务方法
@TxTransaction(isStart = true) 把这个注解答出来
@Service
public class OrderWebServiceImpl implements OrderWebService {
@Autowired
GoodsApi goodsApi;
@Autowired
OrdersApi ordersApi;
@TxTransaction(isStart = true)
@Override
public boolean xiadan(String title, String name) {
Boolean b1 = ordersApi.addOrders(title);
System.out.println("调用微服务,新增订单库...."+b1);
int i=100/0;//模拟
Boolean b2 = goodsApi.addGoods(name);
System.out.println("调用微服务,新增商品...."+b2);
return b1&&b2;
}
}