【RocketMQ】主从模式下的消费进度管理
在【RocketMQ】消息的拉取一文中可知,消费者在启动的时候,会创建消息拉取API对象PullAPIWrapper
,调用pullKernelImpl方法向Broker发送拉取消息的请求,那么在主从模式下消费者是如何选择向哪个Broker发送拉取请求的?
进入pullKernelImpl方法中,可以看到会调用recalculatePullFromWhichNode方法选择一个Broker:
public class PullAPIWrapper {
public PullResult pullKernelImpl(
final MessageQueue mq,
final String subExpression,
final String expressionType,
final long subVersion,
final long offset,
final int maxNums,
final int sysFlag,
final long commitOffset,
final long brokerSuspendMaxTimeMillis,
final long timeoutMillis,
final CommunicationMode communicationMode,
final PullCallback pullCallback
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
// 调用recalculatePullFromWhichNode方法获取Broker ID,再调用findBrokerAddressInSubscribe根据ID获取Broker的相关信息
FindBrokerResult findBrokerResult =
this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
this.recalculatePullFromWhichNode(mq), false);
// ...
if (findBrokerResult != null) {
// ...
// 获取Broker地址
String brokerAddr = findBrokerResult.getBrokerAddr();
if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
brokerAddr = computePullFromWhichFilterServer(mq.getTopic(), brokerAddr);
}
// 发送消息拉取请求
PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
brokerAddr,
requestHeader,
timeoutMillis,
communicationMode,
pullCallback);
return pullResult;
}
}
}
在recalculatePullFromWhichNode
方法中,会从pullFromWhichNodeTable
中根据消息队列获取一个建议的Broker ID,如果获取为空就返回Master节点的Broker ID,ROCKETMQ中Master角色的Broker ID为0,既然从pullFromWhichNodeTable
中可以知道从哪个Broker拉取数据,那么pullFromWhichNodeTable
中的数据又是从哪里来的?
public class PullAPIWrapper {
// KEY为消息队列,VALUE为建议的Broker ID
private ConcurrentMap<MessageQueue, AtomicLong/* brokerId */> pullFromWhichNodeTable =
new ConcurrentHashMap<MessageQueue, AtomicLong>(32);
public long recalculatePullFromWhichNode(final MessageQueue mq) {
if (this.isConnectBrokerByUser()) {
return this.defaultBrokerId;
}
// 从pullFromWhichNodeTable中获取建议的broker ID
AtomicLong suggest = this.pullFromWhichNodeTable.get(mq);
if (suggest != null) {
return suggest.get();
}
// 返回Master Broker ID
return MixAll.MASTER_ID;
}
}
通过调用关系可知,在updatePullFromWhichNode
方法中更新了pullFromWhichNodeTable
的值,而updatePullFromWhichNode
方法又是被processPullResult
方法调用的,消费者向Broker发送拉取消息请求后,Broker对拉取请求进行处理时会设置一个broker ID(后面会讲到),建议下次从这个Broker拉取消息,消费者对拉取请求返回的响应数据进行处理时会调用processPullResult
方法,在这里将建议的BrokerID取出,调用updatePullFromWhichNode
方法将其加入到了pullFromWhichNodeTable
中:
public class PullAPIWrapper {
private ConcurrentMap<MessageQueue, AtomicLong/* brokerId */> pullFromWhichNodeTable =
new ConcurrentHashMap<MessageQueue, AtomicLong>(32);
public PullResult processPullResult(final MessageQueue mq, final PullResult pullResult,
final SubscriptionData subscriptionData) {
PullResultExt pullResultExt = (PullResultExt) pullResult;
// 将拉取消息请求返回的建议Broker ID,加入到pullFromWhichNodeTable中
this.updatePullFromWhichNode(mq, pullResultExt.getSuggestWhichBrokerId());
// ...
}
public void updatePullFromWhichNode(final MessageQueue mq, final long brokerId) {
AtomicLong suggest = this.pullFromWhichNodeTable.get(mq);
if (null == suggest) {
// 向pullFromWhichNodeTable中添加数据
this.pullFromWhichNodeTable.put(mq, new AtomicLong(brokerId));
} else {
suggest.set(brokerId);
}
}
}
接下来去看下是根据什么条件决定选择哪个Broker的。
返回建议的BrokerID
Broker在处理消费者拉取请求时,会调用PullMessageProcessor
的processRequest
方法,首先会调用MessageStore
的getMessage方法获取消息内容,在返回的结果GetMessageResult
中设置了一个是否建议从Slave节点拉取的属性(这个值的设置稍后再说),会根据是否建议从slave节点进行以下处理:
- 如果建议从slave节点拉取消息,会调用
subscriptionGroupConfig
订阅分组配置的getWhichBrokerWhenConsumeSlowly
方法获取从节点将ID设置到响应中,否则下次依旧建议从主节点拉取消息,将MASTER节点的ID设置到响应中; - 判断当前Broker的角色,如果是slave节点,并且配置了不允许从slave节点读取数据(SlaveReadEnable = false),此时依旧建议从主节点拉取消息,将MASTER节点的ID设置到响应中;
- 如果开启了允许从slave节点读取数据(SlaveReadEnable = true),有以下两种情况:
- 如果建议从slave节点拉消息,从订阅分组配置中获取从节点的ID,将ID设置到响应中;
- 如果不建议从slave节点拉取消息,从订阅分组配置中获取设置的Broker Id;
当然,如果未开启允许从Slave节点读取数据,下次依旧建议从Master节点拉取;
订阅分组配置
mqadmin命令的-i
参数可以指定从哪个Broker消费消息(subscriptionGroupConfig
的getBrokerId
返回的值),-w
参数可以指定建议从slave节点消费的时候,从哪个slave消费(subscriptionGroupConfig
的getWhichBrokerWhenConsumeSlowly
方法返回的值):
usage: mqadmin updateSubGroup [-a <arg>] [-b <arg>] [-c <arg>] [-d <arg>] -g <arg> [-h] [-i <arg>] [-m <arg>]
[-n <arg>] [-q <arg>] [-r <arg>] [-s <arg>] [-w <arg>]
-i,--brokerId <arg> consumer from which broker id
-w,--whichBrokerWhenConsumeSlowly <arg> which broker id when consume slowly
public class PullMessageProcessor extends AsyncNettyRequestProcessor implements NettyRequestProcessor {
private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend)
throws RemotingCommandException {
// ...
// 根据拉取偏移量获取消息
final GetMessageResult getMessageResult =
this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), requestHeader.getTopic(),
requestHeader.getQueueId(), requestHeader.getQueueOffset(), requestHeader.getMaxMsgNums(), messageFilter);
if (getMessageResult != null) {
response.setRemark(getMessageResult.getStatus().name());
responseHeader.setNextBeginOffset(getMessageResult.getNextBeginOffset());
responseHeader.setMinOffset(getMessageResult.getMinOffset());
responseHeader.setMaxOffset(getMessageResult.getMaxOffset());
// 是否建议从从节点拉取消息
if (getMessageResult.isSuggestPullingFromSlave()) {
// 选择一个从节点
responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getWhichBrokerWhenConsumeSlowly());
} else {
responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID);
}
// 判断Broker的角色
switch (this.brokerController.getMessageStoreConfig().getBrokerRole()) {
case ASYNC_MASTER:
case SYNC_MASTER:
break;
case SLAVE:
// 如果不允许从从节点读取数据,设置为MasterID
if (!this.brokerController.getBrokerConfig().isSlaveReadEnable()) {
response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY);
responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID);
}
break;
}
// 如果开启了允许从从节点读取数据
if (this.brokerController.getBrokerConfig().isSlaveReadEnable()) {
// 如果建议从从节点拉消息
if (getMessageResult.isSuggestPullingFromSlave()) {
// 获取从节点
responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getWhichBrokerWhenConsumeSlowly());
}
else {
// 获取指定的broker
responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getBrokerId());
}
} else {
// 使用Master节点
responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID);
}
} else {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("store getMessage return null");
}
}
}
是否建议从Slave节点拉取的设置
DefaultMessageStore的getMessage方法中用于获取消息内容,并会根据消费者的拉取进度判断是否建议下次从Slave节点拉取消息,判断过程如下:
- diff:当前CommitLog最大的偏移量减去本次拉取消息的最大物理偏移量,表示剩余未拉取的消息;
- memory:消息在PageCache中的总大小,计算方式是总物理内存 * 消息存储在内存中的阀值(默认为40)/100,也就是说MQ会缓存一部分消息在操作系统的PageCache中,加速访问;
- 如果diff大于memory,表示未拉取的消息过多,已经超出了PageCache缓存的数据的大小,还需要从磁盘中获取消息,所以此时会建议下次从Slave节点拉取;
public class DefaultMessageStore implements MessageStore {
public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset,
final int maxMsgNums,
final MessageFilter messageFilter) {
// ...
// 当前CommitLog的最大偏移量
final long maxOffsetPy = this.commitLog.getMaxOffset();
ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
if (consumeQueue != null) {
minOffset = consumeQueue.getMinOffsetInQueue();
maxOffset = consumeQueue.getMaxOffsetInQueue();
if (maxOffset == 0) {
// ...
} else {
// 根据消费进度获取消息队列
SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset);
if (bufferConsumeQueue != null) {
try {
// ...
// CommitLog最大偏移量减去本次拉取消息的最大物理偏移量
long diff = maxOffsetPy - maxPhyOffsetPulling;
// 计算消息在PageCache中的总大小(总物理内存 * 消息存储在内存中的阀值/100)
long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE
* (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0));
// 是否建议下次去从节点拉取消息
getResult.setSuggestPullingFromSlave(diff > memory);
} finally {
bufferConsumeQueue.release();
}
} else {
// ...
}
}
} else {
status = GetMessageStatus.NO_MATCHED_LOGIC_QUEUE;
nextBeginOffset = nextOffsetCorrection(offset, 0);
}
// ...
return getResult;
}
}
总结
消费者在启动后需要向Broker发送拉取消息的请求,Broker收到请求后会根据消息的拉取进度,返回一个建议的BrokerID,并设置到响应中返回,消费者处理响应时将建议的BrokerID放入pullFromWhichNodeTable,下次拉去消息的时候从pullFromWhichNodeTable中取出,并向其发送请求拉取消息。
消费进度持久化
上面讲解了主从模式下如何选择从哪个Broker拉取消息,接下来看下消费进度的持久化,因为广播模式下消费进度保存在每个消费者端,集群模式下消费进度保存在Broker端,所以接下来以集群模式为例。
在【RocketMQ】消息的拉取一文中可知,集群模式下主要是通过RemoteBrokerOffsetStore
进行消费进度管理的,在持久化方法persistAll
中会调用updateConsumeOffsetToBroker
更新Broker端的消费进度:
public class RemoteBrokerOffsetStore implements OffsetStore {
@Override
public void persistAll(Set<MessageQueue> mqs) {
if (null == mqs || mqs.isEmpty())
return;
final HashSet<MessageQueue> unusedMQ = new HashSet<MessageQueue>();
for (Map.Entry<MessageQueue, AtomicLong> entry : this.offsetTable.entrySet()) {
MessageQueue mq = entry.getKey();
AtomicLong offset = entry.getValue();
if (offset != null) {
if (mqs.contains(mq)) {
try {
// 向Broker发送请求更新消费进度
this.updateConsumeOffsetToBroker(mq, offset.get());
log.info("[persistAll] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}",
this.groupName,
this.mQClientFactory.getClientId(),
mq,
offset.get());
} catch (Exception e) {
log.error("updateConsumeOffsetToBroker exception, " + mq.toString(), e);
}
} else {
unusedMQ.add(mq);
}
}
}
// ...
}
}
由于updateConsumeOffsetToBroker
方法中先调用了findBrokerAddressInSubscribe
方法获取Broker的信息,所以这里先看findBrokerAddressInSubscribe
方法是如何选择Broker的,它需要传入三个参数,分别为:Broker名称、Broker ID、是否只查找参数中传入的那个BrokerID,方法的处理逻辑如下:
- 首先从
brokerAddrTable
中根据Broker的名称获取所有的Broker集合(主从模式下他们的Broker名称一致,但是ID不一致),KEY为BrokerID,VALUE为Broker的地址; - 从Broker集合中根据参数中传入的ID获取broker地址;
- 判断参数中传入的BrokerID是否是主节点,记录在slave变量中;
- 判断获取的Broker地址是否为空,记录在found变量中;
- 如果根据BrokerId获取的地址为空并且参数中传入的BrokerId为从节点,继续轮询获取下一个Broker,并判断地址是否为空;
- 如果此时地址依旧为空并且onlyThisBroker传入的false(也就是不必须选择参数中传入的那个BrokerID),此时获取map集合中的第一个节点;
- 判断获取到的Broker地址是否为空,不为空封装结果返回,否则返回NULL;
public class MQClientInstance {
public FindBrokerResult findBrokerAddressInSubscribe(
final String brokerName, // Broker名称
final long brokerId, // Broker ID
final boolean onlyThisBroker // 是否只查找参数中传入的那个BrokerID
) {
String brokerAddr = null;
boolean slave = false;
boolean found = false;
// 获取所有的Broker ID
HashMap<Long/* brokerId */, String/* address */> map = this.brokerAddrTable.get(brokerName);
if (map != null && !map.isEmpty()) {
brokerAddr = map.get(brokerId);
// 是否是从节点
slave = brokerId != MixAll.MASTER_ID;
// 地址是否为空
found = brokerAddr != null;
// 如果地址为空并且是从节点
if (!found && slave) {
// 获取下一个Broker
brokerAddr = map.get(brokerId + 1);
found = brokerAddr != null;
}
// 如果地址为空
if (!found && !onlyThisBroker) {
// 获取集合中的第一个节点
Entry<Long, String> entry = map.entrySet().iterator().next();
// 获取地址
brokerAddr = entry.getValue();
// 是否是从节点
slave = entry.getKey() != MixAll.MASTER_ID;
// 置为true
found = true;
}
}
if (found) {
// 返回数据
return new FindBrokerResult(brokerAddr, slave, findBrokerVersion(brokerName, brokerAddr));
}
return null;
}
}
回到updateConsumeOffsetToBroker方法,先看第一次调用findBrokerAddressInSubscribe方法获取Broker信息,传入的三个参数分别为:Broker名称、Master节点的ID、true,根据上面讲解的findBrokerAddressInSubscribe
方法里面的查找逻辑,如果查找到Master节点的信息,就正常返回,如果此时Master宕机未能正常查找到,由于传入的Master节点的ID并且onlyThisBroker置为true,所以会查找失败返回NULL。
如果第一次调用为空,会进行第二次调用,与第一次调用不同的地方是第三个参数置为了false,也就是说不是必须选择参数中指定的那个Broker,此时依旧优先查找Master节点,如果Master节点未查找到,由于onlyThisBroker置为了false,会迭代集合选择第一个节点返回,此时返回的有可能是从节点。
总结:消费者会优先选择向主节点发送请求进行消费进度保存,假如主节点宕机等原因未能获取到主节点的信息,会迭代集合选择第一个节点返回,所以消费者也可以向从节点发送请求进行进度保存,待主节点恢复后,依旧优先选择主节点。
public class RemoteBrokerOffsetStore implements OffsetStore {
private void updateConsumeOffsetToBroker(MessageQueue mq, long offset) throws RemotingException,
MQBrokerException, InterruptedException, MQClientException {
// 更新消费进度
updateConsumeOffsetToBroker(mq, offset, true);
}
@Override
public void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean isOneway) throws RemotingException,
MQBrokerException, InterruptedException, MQClientException {
// 第一次调用findBrokerAddressInSubscribe方法获取Broker信息,三个参数分别为:Broker名称、Master节点的ID、true
FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true);
// 如果获取为空,进行第二次调用
if (null == findBrokerResult) {
// 三个参数分别为:Broker名称、Master节点的ID、false
this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, false);
}
if (findBrokerResult != null) {
// 设置请求头
UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader();
requestHeader.setTopic(mq.getTopic());
requestHeader.setConsumerGroup(this.groupName);
requestHeader.setQueueId(mq.getQueueId());
requestHeader.setCommitOffset(offset);
// 发送保存消费进度的请求
if (isOneway) {
this.mQClientFactory.getMQClientAPIImpl().updateConsumerOffsetOneway(
findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5);
} else {
this.mQClientFactory.getMQClientAPIImpl().updateConsumerOffset(
findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5);
}
} else {
throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
}
}
}
主从模式下的消费进度同步
BrokerController
在构造函数中,实例化了SlaveSynchronize
,并在start方法中调用了handleSlaveSynchronize
方法处理从节点的数据同步,
如果当前的Broker是从节点,会注册定时任务,定时调用SlaveSynchronize
的syncAll方法进行数据同步:
public class BrokerController {
private final SlaveSynchronize slaveSynchronize;
public BrokerController(
final BrokerConfig brokerConfig,
final NettyServerConfig nettyServerConfig,
final NettyClientConfig nettyClientConfig,
final MessageStoreConfig messageStoreConfig
) {
// ...
this.slaveSynchronize = new SlaveSynchronize(this);
//...
}
public void start() throws Exception {
if (!messageStoreConfig.isEnableDLegerCommitLog()) {
startProcessorByHa(messageStoreConfig.getBrokerRole());
// 处理从节点的同步
handleSlaveSynchronize(messageStoreConfig.getBrokerRole());
this.registerBrokerAll(true, false, true);
}
}
private void handleSlaveSynchronize(BrokerRole role) {
// 如果是SLAVE节点
if (role == BrokerRole.SLAVE) {
if (null != slaveSyncFuture) {
slaveSyncFuture.cancel(false);
}
this.slaveSynchronize.setMasterAddr(null);
// 设置定时任务,定时进行数据同步
slaveSyncFuture = this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
// 同步数据
BrokerController.this.slaveSynchronize.syncAll();
}
catch (Throwable e) {
log.error("ScheduledTask SlaveSynchronize syncAll error.", e);
}
}
}, 1000 * 3, 1000 * 10, TimeUnit.MILLISECONDS);
} else {
//handle the slave synchronise
if (null != slaveSyncFuture) {
slaveSyncFuture.cancel(false);
}
this.slaveSynchronize.setMasterAddr(null);
}
}
}
在SlaveSynchronize的syncAll
方法中,又调用了syncConsumerOffset
方法同步消费进度:
- 向主节点发送请求获取消费进度数据;
- 从节点将获取到的消费进度数据进行持久化;
public class SlaveSynchronize {
public void syncAll() {
this.syncTopicConfig();
// 同步消费进度
this.syncConsumerOffset();
this.syncDelayOffset();
this.syncSubscriptionGroupConfig();
}
private void syncConsumerOffset() {
String masterAddrBak = this.masterAddr;
if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) {
try {
// 向主节点发送请求获取消费进度信息
ConsumerOffsetSerializeWrapper offsetWrapper =
this.brokerController.getBrokerOuterAPI().getAllConsumerOffset(masterAddrBak);
// 设置数据
this.brokerController.getConsumerOffsetManager().getOffsetTable()
.putAll(offsetWrapper.getOffsetTable());
// 将获取到的消费进度数据进行持久化
this.brokerController.getConsumerOffsetManager().persist();
log.info("Update slave consumer offset from master, {}", masterAddrBak);
} catch (Exception e) {
log.error("SyncConsumerOffset Exception, {}", masterAddrBak, e);
}
}
}
}
参考
丁威、周继锋《RocketMQ技术内幕》
RocketMQ 主从同步若干问题答疑
RocketMq 订阅分组创建和删除
RocketMQ版本:4.9.3
【RocketMQ】主从模式下的消费进度管理的更多相关文章
- mysql主从模式下在主库上的某些操作不记录日志的方法
mysql主从模式下在主库上的某些操作不记录日志的方法 需求场景: 在主库上的需要删除某个用户,而这个用户在从库上不存在(我在接手一个业务的时候,就遇到主从架构用户授权不一致的情况,主库比较全,而从库 ...
- 【mq读书笔记】消费进度管理
从前2节可以看到,一次消费后消息会从ProcessQueue处理队列中移除该批消息,返回ProcessQueue最小偏移量,并存入消息进度表中.那消息进度文件存储在哪合适呢? 广播模式:同一个消费组的 ...
- Mycat主从模式下的读写分离与自动切换
1. 机器环境 192.168.2.136 mycat1 192.168.2.134 mydb1 192.168.2.135 mydb2 2在mysql1.mysql2上安装mysql 更改root用 ...
- CentOS7 安装配置RocketMQ --主从模式(master-slave)异步复制
机器信息 192.168.119.129 主 192.168.119.128 从 配置host[两台机器] vim /etc/hosts 添加 192.168.119.129 rocketmq-nam ...
- Standalone模式下,通过Systemd管理Flink1.11.1的启停及异常退出
Flink以Standalone模式运行时,可能会发生jobmanager(以下简称jm)或taskmanager(以下简称tm)异常退出的情况,我们可以使用Linux自带的Systemd方式管理jm ...
- RocketMQ 主从同步若干问题答疑
目录 1.初识主从同步 2.提出问题 3.原理探究 3.1 RocketMQ主从读写分离机制 3.2 消息消费进度同步机制 4.总结 温馨提示:建议参考代码RocketMQ4.4版本,4.5版本引入了 ...
- RocketMQ之消费者启动与消费流程
vivo 互联网服务器团队 - Li Kui 一.简介 1.1 RocketMQ 简介 RocketMQ是由阿里巴巴开源的分布式消息中间件,支持顺序消息.定时消息.自定义过滤器.负载均衡.pull/p ...
- Redis 单例、主从模式、sentinel 以及集群的配置方式及优缺点对比(转)
摘要: redis作为一种NoSql数据库,其提供了一种高效的缓存方案,本文则主要对其单例,主从模式,sentinel以及集群的配置方式进行说明,对比其优缺点,阐述redis作为一种缓存框架的高可用性 ...
- Redis 单机模式,主从模式,哨兵模式(sentinel),集群模式(cluster),第三方模式优缺点分析
Redis 的几种常见使用方式包括: 单机模式 主从模式 哨兵模式(sentinel) 集群模式(cluster) 第三方模式 单机模式 Redis 单副本,采用单个 Redis 节点部署架构,没有备 ...
- redis(二)redis的主从模式和集群模式
redis(二)redis的主从模式和集群模式 主从模式 集群模式 主从模式 redis的主从模式,指的是针对多台redis实例时候,只存在一台主服务器master,提供读写的功能,同时存在依附在这台 ...
随机推荐
- 搞透 IOC,Spring IOC 看这篇就够了!
IOC与AOP属于Spring的核心内容,如果想掌握好Spring你肯定需要对IOC有足够的了解 @mikechen IOC的定义 IOC是Inversion of Control的缩写,多数书籍翻译 ...
- [题解] Atcoder Beginner Contest ABC 265 Ex No-capture Lance Game DP,二维FFT
题目 首先明确先手的棋子是往左走的,将其称为棋子1:后手的棋子是往右走的,将其称为棋子2. 如果有一些行满足1在2右边,也就是面对面,那其实就是一个nim,每一行都是一堆石子,数量是两个棋子之间的空格 ...
- cmd常用命令介绍
一.cdm命令介绍:CMD命令是一种命令提示符,CMD是command的缩写,即命令提示符(CMD),位于C:\Windows\System32的目录下,是在OS/2,Win为基础的操作系统(包括Wi ...
- Windows常用快捷键及基本的Dos命令
Windows 常用快捷键 Ctrl + C: 复制 Ctrl + V: 粘贴 Ctrl + A: 全选 Ctrl + X: 剪贴 Ctrl + Z: 撤销 Ctrl + S: 保存 Alt + F4 ...
- day09-2视图和用户权限
视图和用户权限 1.视图(view) 看一个需求 emp表的列信息很多,有些信息是个人重要信息(比如:sal.comm.mgr.hiredate),如果我们希望某个用户只能查询emp表的empno.e ...
- 使用HTML表单收集数据
1.什么是表单 在项目开发过程中,凡是需要用户填写的信息都需要用到表单. #2.form标签 在HTML中我们使用form标签来定义一个表单.而对于form标签来说有两个最重要的属性:action和m ...
- 小程序 wx.navigateTo和 wx.redirectTo区别
wx.navigateTo 官方解释: 意思就是说. A页面跳转B页面 B页面做了操作,点击保存,再跳转回A页面 此时,如果点击左上返回按钮,仍然可以跳转回B页面,而且里面的数据是操作之前的数据 wx ...
- springboot项目中使用shiro实现用户登录以及权限的验证
欢迎大家加入我的社区:http://t.csdn.cn/Q52km 社区中不定时发红包 更加高级的验证用户权限:用户表.角色表.权限表.多表联合:https://blog.csdn.net/weixi ...
- maven 重复依赖不同版本 选择规则
maven 重复依赖不同版本 选择规则 本篇主要来看看 maven 对于 重复依赖的jar的不同版本时候 它内部的选择规则, 很多时候我们在搭建环境的时候 不注意就会存在依赖冲突等问题 那依赖冲突的时 ...
- Unity坐标系入门
一.坐标系的概念 Unity 世界坐标系采用左手坐标系,大拇指指向X轴(红色),食指指向Y轴(黄色),中指向手心方向歪曲90度表示Z轴(蓝色),同时Z轴也是物体前进方向,下图表示Unity的四种坐标系 ...