面试题总结 20231024

1.桶排序的应用案例:上亿元素排行榜

 step1: 桶排序本质是一种分治算法

面试题总结 20231024_第1张图片

step2:每个桶都代表了一个元素的范围

面试题总结 20231024_第2张图片

step3:每个桶中的元素都排好序后,取出来,这样子就有序了

面试题总结 20231024_第3张图片

2.简述你们框架中用到的线程模型

1.actor思想(单线程处理)

2.xdb加锁(类似的还有mysql的锁机制)

3.解释下你们xdb中get方法拿锁的流程 和 拿锁是tryLock还是lock,业务执行过长怎么办?时用到的锁是什么?(以秘宝为例子)

1.首先是GatewayHandler收到玩家请求

public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            if(msg == null) {
                return;
            }

            if(GameServer.isServerClosed()) {
                return;
            }

            ProtobufMessage message = (ProtobufMessage) msg;
            session.parseAndDispatchMessage(message);
}

2.ClientSession是绑定了根据userId,玩家登录后就也确定了roleId。 所以将上一步的请求和这个玩家信息封装为上下文如:OnlineMsgParam,然后嵌套Instance,扔到xdb线程池中执行

 // 这个是使用xdb.Executor.getInstance()是单例类,是支持了Timeout
        xdb.Executor.getInstance().execute(() ->
                client2LogicMsgInstance.handle(message, "userId", userId));

其中这个xdb.Executor.getInstance()是一个支持超时的线程池TimeoutExecutor

execute方法:

	public void execute(Runnable command) {
		super.execute(xdb.Angel.decorateRunnable(command, defaultTimeout));
	}

可见这是一个被包装为支持超时的任务了。

3.这个任务中很可能是一个修改操作,比如:领奖,那么这个业务Task被执行时,里面肯定是触发了一个submit的提交,咱们看下实现:

    @MsgReceiver(MsgArtifact.CSAddPositionExp.class)
    public static void onCSAddPositionExp(OnlineMsgParam param) {
         
        // 这个业务体肯定是在xdb线程中,同时也接收了超时检测,但是我想这个超时检测仅仅是:
        // 这个方法体的,毕竟submit的执行还是异步的。所以一般不会超时,除非带有select这种超时了

        new PAddArtifactBaseExp(param.getHumanId(), equips).submit();
    }

submit的实现如下:

    public final Future submit() {
        // 首先验证下是不是已经在事务内了,在事务内了是不能submit了
        verify();

        // 看着是new了一个ProcedureFuture对象,实际还是任务的提交
        return new ProcedureFuture(this);
    }

看下构造方法:

    public ProcedureFuture(P p) {
        // 保存下事务
        this.p = p;

        // 扔到带有超时检测的线程池中
        // 实现的是Future接口
        future = Xdb.executor().getProcedureTimeoutExecutor()
                .submit(this, p, p.getConf().getMaxExecutionTime());

        // 默认情况下,啥都不干
        this.done = null;
    }

可见是把当前事务又包装了下,这个submit是自己写的,注意:主角Angle登场了。

其实还是调用的java线程池的submit,只不过是多了一层Angle的包装:

	public  Future submit(Runnable task, T result, long timeout) {
		xdb.Worker.debugHunger(this);
		return super.submit(xdb.Angel.decorate(task, result, timeout));
	}

这个Angle包装下是为了啥呢,接下来看:

  public static  Callable decorate(Runnable task, V result, long timeout) {
        // 将Runnable包装为Callable
        final Callable callable = Executors.callable(task, result);

        // 默认肯定是有超时检测的,因此这里就是:TimeoutCallable
        return timeout > 0 ? new TimeoutCallable<>(callable, timeout) : callable;
    }

可见是为了这个TimeoutCallable又进行了包装(又使用TimeoutManager带上了超时检测)

    @Override
    public V call() throws Exception {
        if (timeout > 0) {
            runner = Thread.currentThread();

            // 将此任务开始执行前扔到TimeoutManager中,如果到时候没移除,则说明此任务执行超时
            final TimeoutManager tm = TimeoutManager.getInstance();
            tm.schedule(this, timeout);

            try {
                // 任务真正开始执行
                return inner.call();
            } finally {
                // 执行完毕后,移除
                tm.remove(this);
                runner = null;
            }
        } else {
            return inner.call();
        }
    }

至此,是不是有点迷糊了,再次后头看一下,其实这个inner.call()执行的实际是啥呢?其实就是:

ProcedureFuture的方法体,也就是最初:new ProcedureFuture的地方,我们看下这个执行体(只不过这个执行体又被包装了支持了超时检测)

@Override
    public void run() {
        // 存储过程开始执行,执行次数+1
        ++ranTimes;

        try {
            // 创建事务和执行存储过程。
            try {
                // 核心方法:所以这个里面调用的还是call方法
                Transaction.create().perform(p);
            } finally {
                // safe if create fail
                Transaction.destroy();
            }

            // 正常存储过程执行结束
            // 但是目前看done变量为空,等于这句啥也没做
            done();
        } catch (XLockDead e) { // 这个异常何时被抛出来呢?其实就是:Lockkey中的无参lock方法使用的是lockInterruptibly,在被超时打断的时候会跑出来
            /** @see Lockey#lock() */

            // 重试次数过多
            if (ranTimes >= p.getConf().getRetryTimes()) {
                done();

                // 达到最大重复次数.报告最终错误.
                throw new XAngelError();
            }

            // 下面是发生死锁了,随机一个时间,进行重试
            int delay = Xdb.random().nextInt(p.getConf().getRetryDelay());

            // 再次提交任务到线程池
            future = Xdb.executor().getScheduledTimeoutExecutor().schedule(
                    Executors.callable(this, p),
                    delay,
                    TimeUnit.MILLISECONDS,
                    p.getConf().getMaxExecutionTime());

            // 报告死锁错误,future打断当前的监视对象,重新监视。
            throw e;
        } catch (Error error) {
            done();
            throw error;
        } catch (Throwable e) {
            done();
            // 有其他方法,不需要包装一下,直接扔出去吗?
            throw new XError(e);
        }
    }

我们看下Transaction的perform方法干了啥?其实最重要的就是调用了call方法,从而创建事务,

并且执行我们的process方法

 public void perform(Procedure p) throws Throwable {
        try {
            // 总数 = .True(未统计此项) + .False + .Exception
            //counter.increment(p.getClass().getName());
            totalCount.incrementAndGet();

            // flush lock . MEMORY类型的表本来不需要这个锁,为了不复杂化流程,不做特殊处理。
            Lock flushLock = Xdb.getInstance().getTables().flushReadLock();
            flushLock.lockInterruptibly();
            try {
                // 重点方法call!!!
                if (p.call()) {
                    if (_real_commit_() > 0) {
                        logNotify(p);
                        // else : 没有修改,不需要logNotify。至此过程处理已经完成了。
                    }
                } else {
                    // 执行逻辑返回false统计
                    //counter.increment(p.getClass().getName() + ".False");
                    totalFalse.incrementAndGet();
                    _last_rollback_(); // 应用返回 false,回滚
                }
            } catch (Throwable e) {
                // 未处理的异常,回滚
                _last_rollback_();
                throw e;
            } finally {
                // 有多把锁
                if (deadLockDetection && lockList.size() > 1) {
                    //死锁风险检测
                    deadlockDetection(p.getClass().getName());
                }

                this.doneRunAllTask();
                this.finish();
                flushLock.unlock();
            }

        } catch (Throwable e) {
            p.setException(e);
            p.setSuccess(false);
            // 执行异常统计
            //counter.increment(p.getClass().getName() + ".Exception");
            totalException.incrementAndGet();
            // 所有的异常错误都应该处理,尽量不抛到这里。这里仅记录日志。
            Trace.error("Transaction Perform Exception " + p.getClass().getName(), e);
            throw e;
        }
    }

然后进入到高潮部分,也就call方法,也就是调用我们业务层的process,从而真正业务执行部分(同时在业务异常或者返回false时将本地缓存回滚,也就是log删除掉)

public boolean call() {
        // 当前如果不在事务内
        if (Transaction.current() == null) {
            try {
                // perform 将回调本函数,然后执行事务已经存在的分支。
                // 何时被加上事务的呢?其实就是这个Transaction.create()中执行的,当前ThreadLocal没存,则设置下
                Transaction.create().perform(this);
            } catch (Throwable e) {
                // this.setException(e); 在 Transaction.perform 里面会保存异常。这里没什么事可做了。
            } finally {
                Transaction.destroy();
                this.fetchTasks();
            }
            return this.isSuccess();
        }

        // 执行到这里必然是处于事务中了,则记录下保存点
        int savepoint = beginAndSavepoint();

        // 捕捉所有异常,在发生异常和process返回false时,回滚到过程开始的保存点。
        // 不捕捉错误,所有的错误抛到外层。
        try {
            if (process()) {
                commit();
                this.setSuccess(true);
                return true;
            }
        } catch (Exception ex) {
            this.setException(ex);
            logErrorFunc.accept(ex);
        }

        // 进行业务的回滚
        rollback(savepoint);

        return false;
    }

4.理解xdb中怎么拿锁的

经过上面的分析,我们看出来,其实就是业务的执行时,仅仅是使用ThreadLocal保存了当前new出来的事务对象,然后接着执行我们的process方法了,里面就是我们游戏层的业务实现了,我们分析下process方法如何拿锁的,我们直接看秘宝的process方法:

1.映入眼帘的肯定是这一句

ArtifactBean artifactBean = Artifact.get(humanId);

2.看一下get方法

	public static xbean.ArtifactBean get(Long key) throws Exception {
		return _Tables_.getInstance().artifact.get(key);
	}

3.这个方法是重载的

public final V get(K key) throws Exception {
        return get(key, true);
    }

4.接下来看实现

    public final V get(K key, boolean holdNull) throws Exception {
        if (null == key) {
            throw new NullPointerException("key is null");
        }

        countGet.incrementAndGet();

        // 从事务本身里先进行查询,也就是在事务内拿过一次锁之后,以后不会再重复拿了
        final Transaction currentT = Transaction.current();

        // 先从本地事务哪个普通的Map对象中拿缓存.这段代码算是优化了
        TRecord rCached = currentT.getCachedTRecord(this, key);
        if (rCached != null) {
            return rCached.getValue();
        }

        // 事务缓存中没拿到,就生成一把锁
        Lockey lockey = Lockeys.get(this, key);

        // 这里调用的是:lockInterruptibly方法
        lockey.lock();
        try {
            // 这个是实现LRU算法的,从本地缓存取数据,对应的类是:TTableCacheLRU,取缓存会有一次synchronized的调用,所以上面还有一份事务内的缓存加快访问
            // LRU底层是:包装的LinkedHashMap实现,删除策略是自己实现的
            TRecord r = cache.get(key);

            // 缓存中不存在
            if (null == r) {
                // 记录下缓存miss了
                countGetMiss.incrementAndGet();

                // 缓存中也没有,那就查询sql了
                V value = _find(key);

                // sql也没查询到
                if (null == value) {
                    countGetStorageMiss.incrementAndGet();
                    if (holdNull) {
                        currentT.add(lockey);
                    }
                    return null;
                }

                // sql查询到了
                r = new TRecord(this, value, lockey, TRecord.State.INDB_GET);

                // 先记录数据到LRU缓存中
                cache.addNoLog(key, r);
            }

            // 下面其实是记录一次缓存到本地事务中,这样子不是每次都从LRU cache中取,可以减少一次锁的访问
            // 重点:记录当前事务拿到的锁! 死锁检测就从这里入手了
            // 注意: 里面还是有一次lock。 所以下面finally会先unlock一次
            currentT.add(lockey);

            // 记录下本次事务用过的缓存
            currentT.addCachedTRecord(this, r);

            // 返回取到的值
            return r.getValue();
        } finally {
            lockey.unlock();
        }
    }

分析:

可以看出来,当前缓存的话,是根据Transaction中一个普通的Map中拿到的,首先根据当前的表名字和key取缓存,毕竟会涉及到多张表,所以是要传入表对象的。

这样子其实有2份缓存:1份是事务内的缓存,一份是:根据表+synchronized。

这样子拿过缓存后,就不会再加锁了直接取缓存。否则会查询缓存。

缓存查询不到,查sql,查到了,则记录到本地事务中

前面分析了,何时释放所有的锁呢?

Transaction.java

    /**
     * 结束事务,释放所有锁并且清除,清除wrapper。
     */
    private void finish() {
        wrappers.clear();

        // 没有按照lock的顺序unlock。
        for (Lockey lockey : locks.values()) {
            // Trace.debug("unlock " + lockey);
            try {
                lockey.unlock();
            } catch (Throwable e) {
                Trace.fatal("unlock " + lockey, e);
            }
        }
        locks.clear();
        cachedTRecord.clear();
    }

5.理解xdb中的超时检测机制

如何进行超时检测呢?

在Exector初始化的时候,就负责开启了一个定时器进行超时检测的处理

      this.scheduled.scheduleWithFixedDelay(
                // 执行超时检测
                xdb.util.TimeoutManager.getInstance(),

                timeoutPeriod,
                timeoutPeriod,
                TimeUnit.MILLISECONDS);

检测到超时后,进行打断,咱们的Xdb线程池中的线程都是Worker线程:

 @Override
    public void onTimeout() {
        final Thread r = runner;

        // 如果这个r为null,则说明已经执行完了
        if (r != null) {
            if (r instanceof Worker) {
                ((Worker) r).angelInterrupt();
            } else {
                r.interrupt();
            }
        }
    }

打断方法实现如下:

   /**
     * 这个是Worker被打断时,多执行一个标记
     */
    public void angelInterrupt() {
        angel.set(true);
        super.interrupt();
    }

Lockey中使用的是可被打断的lockInterruptibly:

    /**
     * 这个非常的重要,
     */
    public final void lock() {
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException ex) {
            // 这里我认为其实未必就是真的死锁,有可能业务执行繁忙超时后,也会被打断?
            if (Worker.angelInterrupted()) {
                throw new XLockDead();
            }

            throw new XLockInterrupted(this.toString());
        }
    }

3.这样子在ArtifactMsgHandler的一个方法处理器中,就会到一个xdb线程池中执行。

6.简述Recast Nav中障碍的实现

1.

7.简述技能系统的实现

1.

8.压测都发现了什么问题?

1.

你可能感兴趣的:(#,面试题,java,1024程序员节)