【后端开发】后端常见场景题

非开发科班生遇到喜欢问场景的面试官可太难了,基本秒跪。来临时抱抱佛脚,总结下常见场景面试题,部分是面试中遇到的,部分是网上搜集的。


TOP K问题


10亿个数据中找出最大的10000个?——最小堆

最小堆法

  1. 先拿10000个数建堆
  2. 然后逐个添加剩余元素
  3. 如果大于堆顶的数(10000中最小的),将这个数替换堆顶,并调整结构使之仍然是一个最小堆
  4. 遍历完后,堆中的10000个数就是所需的最大的10000个。
  • 复杂度分析:时间复杂度是O(mlogm),算法的时间复杂度为O(nmlogm)(n为10亿,m为10000)。

优化方法

  • 如果内存受限:可以直接在内存总使用Hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑可以采用最小堆,最后一个线程将结果归并。

    进一步优化:
    该方法存在一个瓶颈会明显影响效率,即数据倾斜。每个线程的处理速度可能不同,快的线程需要等待慢的线程,最终的处理速度取决于慢的线程。而针对此问题
    解决的方法是,将数据划分成c×n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,直到所有数据处理完毕,最后由一个线程进行归并

  • 如果含较多重复值:先用hash / 依图法去重,可大大节省运算量

有几台机器存储着几亿淘宝搜索日志,你只有一台 2g 的电脑,怎么选出搜索热度最高的十个?

针对top k类文本问题,通常比较好的方案是【分治+trie树/hash+小顶堆】,即先将数据集按照hash方法分解成多个小数据集,然后使用trie树或者hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出频率最高的前K个数,最后在所有top K中求出最终的top K。

  • 拆分成n多个文件:以首字母区分,不同首字母放在不同文件,长度仍过长的继续按照次首字母进行拆分。这样一来,每个文件的每个数据长度相同且首字母尾字母也相同,就能保证数据被独立的分为了n个文件,且各个文件中不存在关键词的交集。

  • 分别词频统计:对于每个文件,使用hash或者Trie树进行进行词频统计

  • 小顶堆排序:依次处理每个文件,并逐渐更新最大的十个词


提取某日访问次数最多的IP地址

将去进行 %1024 取模散列到1024个文件中**(划分子文件方法之一)**
采用hashmap对其进行次数统计 最后用排序算法进行排序。


数据太多,内存不够,找到最热门的K个数据

首先对数据进行预处理,采用hash表统计次数 然后再排序


海量数据排序、压缩问题?


重要方法——位图法 Bitmap

  • 位图的基本概念:用一个位(bit)来标记某个数据的存放状态。例如,有{2, 4, 5, 6, 67, 5}这么几个整数,我维护一个 00…0000(共67位)0/1字符串,1表示该索引(=数据值)处存在数,0则表示不存在。
  • 应用:位图法可以用于海量数据排序,海量数据去重,海量数据压缩
  • 优点:针对于稠密的数据集可以很好体现出位图法的优势,内存消耗少,速度较快
  • 缺点:不适用于稀疏数据集,比如我们有一个长度为10的序列,最大值为20亿,则构造位串的内存消耗将相当大250M,而实际却只需要40个字节,此外位图法还存在可读性差等缺点。

非重复排序

假设我们有一个不重复的整型序列{n1, n2, ... ,nn},假设最大值为nmax,则我们可以维护一个长度为nmax的位串。主要过程就是2步:

  • 第一遍遍历整个序列,将出现的数字在位串(java中可以用数组实现)中对应的位置置为1;
  • 第二遍遍历位图,依次输出值为1的位对应的数字,这些1所在的位串中的位置的索引代表序列数据,1出现的先后位置则代表序列的大小关系。

重复排序——保留 / 不保留重复值

  • 保留重复值:同上,只是子串中不只存在0/1,实际数量为多少,则值为多少.输出时,值为多少则输出多少遍
  • 不保留:略。

数据压缩

前提:数据中存在大量的冗余值
基本思路就是使用某个子串存储原数据中的海量值


如何用redis存储统计1亿用户一年的登陆情况,并快速检索任意时间窗口内的活跃用户数量

在redis 2.2.0版本之后,新增了一个位图数据。redis单独对bitmap提供了一套命令。可以对任意一位进行设置和读取。所以可以在位图中使用1表示活跃。

bitmap的核心命令:

  • SETBIT:设置某位为1
    语法:SETBIT key offset value
    例如:

    setbit abc 5 1 ----> 00001
    setbit abc 2 1 ----> 00101
    
  • GETBIT:获取某位的值
    语法:GETBIT key offset
    例如:

    getbit abc 5 ----> 1
    getbit abc 1 ----> 0
    

bitmap的其他命令还有bitcount,bitpos,bitop等命令。都是对位的操作。

  • 获取某一天id为88000的用户是否活跃:getbit 2020-01-01 88000 [时间复杂度为O(1)]
  • 统计某一天的所有的活跃用户数:bitcount 2019-01-01 [时间复杂度为O(N)]
    -统计某一段时间内的活跃用户数,需要用到bitop命令。这个命令提供四种位运算,AND(与),(OR)或,XOR(亦或),NOT(非)。我们可以对某一段时间内的所有key进行OR(或)操作,或操作出来的位图是0的就代表这段时间内一次都没有登陆的用户。那只要我们求出1的个数就可以了

以下例子求出了2019-01-01到2019-01-05这段时间内的活跃用户数。

bitop or result 2019-01-01 2019-01-02 2019-01-03 2019-01-04 2019-01-05 [时间复杂度为O(N)]
bitcount result

从时间复杂度上说,无论是统计某一天,还是统计一段时间。在实际测试时,基本上都是秒出的。符合我们的预期。


海量文本去重——simhash法

参考文章


资源 vs 请求问题


如果一个外卖配送单子要发布,现在有200个骑手都想要接这一单,如何保证只有一个骑手接到单子?

  1. 如果只是单机,采用volatile关键字修饰该订单采用CAS操作对其进行乐观锁操作。

    • volatile保证可见性
    • CAS保证原子性
  2. 采用redis,zookeeper分布式锁加锁。

  3. 消息队列 实现幂等接口
    如果只是单机,采用volatile关键字修饰该订单采用CAS操作对其进行乐观锁操作。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class sub1 {
    static int USER_NUM=10000;//模拟并发数,也就是有这么多个用户一起抢单
    static CountDownLatch countDownLatch=new CountDownLatch(USER_NUM);//用这个来实现所有线程在同一起跑线上
    // AtomicInteger.getAndDecrement底层就是CAS
    static volatile AtomicInteger number=new AtomicInteger(10);//有限的资源数
    static Ticket ticket=new Ticket();

    public static void main(String[] args) throws InterruptedException{
        Buy();
    }

    public static void Buy() throws InterruptedException {
        for (int i = 0; i < USER_NUM; i++) {
            new Thread(new MyThread()).start();
            if(i==USER_NUM){
                Thread.currentThread().sleep(1000);
            }
            countDownLatch.countDown();
        }
        Thread.currentThread().sleep(2000);

    }

    public static class MyThread implements Runnable{

        @Override
        public void run() {
            try{
                countDownLatch.await();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            ticket.sale();
        }
    }
    public static class Ticket {
        public void sale(){
            int n=number.getAndDecrement();
            if(n>0){
                System.out.println(Thread.currentThread().getName() + "抢到了第  " + n + "  张票");
            }
        }
    }
}

多个微信用户抢红包

类似于秒杀系统

  1. 数据库加乐观锁、悲观锁
  2. 在逻辑处理界面加分布式锁
  3. 消息队列

❓1000个任务分给10个人做

全局队列,每一个人都从一个队列中取
分成10个队列对应每一个人


设计类问题


你如何设计一个消息队列?

回答一:开放式回答,比较粗糙

  • 首先这个 mq 得支持可伸缩性
    就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
  • 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?
    那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。
  • 其次你考虑一下你的 mq 的可用性啊?
    这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。
  • 能不能支持数据 0 丢失啊?
    可以的,参考我们之前说的那个 kafka 数据零丢失方案。

回答二:流程式回答,比较详细
参考文章


其他


怎么判别淘宝刷单?怎么检验手段效果?

商家角度:

  • 营业额远高于历史平均数据
  • 营业额远高于行业平均数据
  • 下单的用户很多均为存在异常行为被监控的账户

顾客角度(账号维度):判断是否存在异常行为

  • 单次交易行为:是否精准搜索、货比三家、页面停留时间等
  • 近期购物成功率:远高于历史时期平均购物成功率
  • 是否可能为垫付:短时间支付宝账户内收到与下单金额相同的金额
  • 下单的店铺很多均为存在异常行为被监控的账户

商家与顾客:

  • 存在相近或者共同的网络环境:如ip、wifi等

如何把一个文件快速下发到 100w 个服务器?

采用p2p网络形式,比如树状形式,网状形式,单个节点既可以从其他节点接收服务又可以向其他节点提供服务。

对于树状传递,在100W台服务器这种量级上,可能存在两个问题:

  • 如果树上的某一个节点坏掉了,那么从这个节点往下的所有服务器全部宕机。
  • 如果树中的某条路径,传递时间太长了(网络中,两个节点间的传递速度受很多因素的影响,可能相差成百上千倍),使得传递效率退化。

改进:100W台服务器相当于有100W个节点的连通图。那么我们可以在图里生成多颗不同的生成树,在进行数据下发时,同时按照多颗不同的树去传递数据。这样就可以避免某个中间节点宕机,影响到后续的节点。同时这种传递方法实际上是一种依据时间的广度优先遍历,可以避免某条路径过长造成的效率低下。


如何实现两个线程交替打印

第一种方法:加锁,并使用volatile boolean类型的标志来控制线程执行

public class sub2{
    static volatile int num=1;
    static volatile boolean flag=true;
    public static void main(String[] args) {
        Object obj=new Object();
        new Thread(()->{
            while(num<=100){
                if(flag){
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    num++;
                    flag=false;
                }
            }
        },"A").start();
        new Thread(()->{
            while(num<=100){
                if(!flag){
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    num++;
                    flag=true;
                }
            }
        },"B").start();
    }
}

第二种方法:加锁,一个线程执行完就等待并唤醒另一个线程

public class sub2{
    static volatile int num=1;
    public static void main(String[] args) {
        Object obj=new Object();
        new Thread(()->{
            while(num<=100){
                synchronized (obj){
                    obj.notifyAll();
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    num++;
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"A").start();
        new Thread(()->{
            while(num<=100) {
                synchronized (obj){
                    obj.notifyAll();
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    num++;
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"B").start();
    }
}
  • 10
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis是一种基于内存的键值对存储数据库,常用于高性能的数据缓存、分布式锁、消息队列、计数器等场景。 在后端Java开发中,Redis的使用场景包括但不限于以下几种: 1. 缓存:将频繁查询的数据缓存到Redis中,提高访问速度和性能; 2. 分布式锁:通过Redis实现分布式锁,保证多个服务实例对同一个资源进行互斥访问; 3. 消息队列:通过Redis实现发布/订阅模式或者消息队列,实现异步处理、解耦和削峰填谷等功能; 4. 计数器:通过Redis实现计数器,统计在线用户数、PV/UV等指标。 下面是一个使用Redis进行缓存的示例代码: ```java public class RedisCacheManager { private JedisPool jedisPool; public RedisCacheManager(String host, int port) { jedisPool = new JedisPool(host, port); } public void set(String key, Object value, int expireTime) { try (Jedis jedis = jedisPool.getResource()) { jedis.set(key.getBytes(), SerializationUtils.serialize(value)); if (expireTime > 0) { jedis.expire(key.getBytes(), expireTime); } } } public Object get(String key) { try (Jedis jedis = jedisPool.getResource()) { byte[] value = jedis.get(key.getBytes()); if (value != null) { return SerializationUtils.deserialize(value); } } return null; } } ``` 这段代码通过JedisPool获取Jedis实例,然后通过set方法将对象序列化后存入Redis缓存中,并设置过期时间;通过get方法从Redis中获取对象并进行反序列化操作。需要注意的是,使用完Jedis实例后需要及时关闭,否则会导致连接泄漏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值