编程导航系统设计话题讨论

系统设计

19 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

浅析布隆过滤器

## 什么是布隆过滤器 **布隆过滤器(Bloom Filter)** 是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。 简单来说是用于快速判断一个元素是否存在于一个集合中。具体来说,布隆过滤器包含一个位数组和一组哈希函数。位数组的初始值全部置为 0。在插入一个元素时,将该元素经过多个哈希函数映射到位数组上的多个位置,并将这些位置的值置为 1。 1字节(Byte)= 8位(Bit) <img src="https://pic.code-nav.cn/post_picture/1768274766864605186/jbhaaUnad7eXY4oo.webp" alt="CleanShot 2024-08-28 at 20.04.36@2x" width="100%" /> 在查询一个元素是否存在时,会将该元素经过多个哈希函数映射到位数组上的多个位置,如果所有位置的值都为 1,则认为元素存在;如果存在任一位置的值为 0,则认为元素不存在。 ### 布隆过滤器的优点 1. 支持海量数据场景下高效判断元素是否存在。 2. 布隆过滤器存储空间小,并且节省空间,不存储数据本身,仅存储hash结果取模运算后的位标记。 3. 不存储数据本身,比较适合某些保密场景。 ### 布隆过滤器的缺点 1. 不存储数据本身,所以只能添加但不可删除,因为删掉元素会导致误判率增加。 2. 由于存在hash碰撞,匹配结果如果是“存在于过滤器中”,实际不一定存在。 3. 当容量快满时,hash碰撞的概率变大,插入、查询的错误率也就随之增加了。 ### 对布隆过滤器的一些理解 - 布隆过滤器要设置初始容量。容量设置越大,冲突几率越低。 - 布隆过滤器会设置预期的误判值。 布隆过滤器的误判是否可以接受? **布隆过滤器的误判是可以接受的,前提是你理解并接受其使用场景的特点和限制。** 1. 误判的影响 - 布隆过滤器可能会错误地报告某个元素存在,尽管实际上它并不存在。这种情况称为“假阳性”。 - 假阳性的概率与布隆过滤器的大小、哈希函数的数量以及存储的元素数量有关。通过合理的设计,这个误判概率可以被控制在一个较低的范围内。 2. 可以接受的原因 - **空间效率**:布隆过滤器在内存占用方面非常高效,特别适合大规模数据集合的判断场景。在内存资源有限的情况下,布隆过滤器能够大大减少存储需求。 - **查询速度**:布隆过滤器的查询操作非常快速,通常只需要O(1)的时间复杂度,非常适合实时查询的场景。 - **适用场景**:布隆过滤器被广泛应用于需要快速判断是否存在的场景,并且这些场景能够容忍一定的假阳性。例如,网络爬虫中用来过滤已经访问过的URL,数据库的缓存系统中用来判断数据是否在缓存中等。这些场景中,假阳性只会导致不必要的进一步检查,而不会造成致命的错误。 3. 不适用的场景 - 需要绝对精确性的场景下,布隆过滤器的误判不可接受。例如,在金融交易、医疗数据处理、密码学等领域,误判可能导致严重的后果,因此不适合使用布隆过滤器。 - 如果误判会带来较大的系统开销或业务逻辑复杂性增加,那么也需要谨慎选择是否使用布隆过滤器。 就拿用户注册来说,布隆过滤器的误判我们可以接受吗? 可以接受。为什么?因为用户名不是特别重要的数据,如果说我设置用户名为 test,系统返回我不可用,那我大可以在 test 的基础上再加一个 666,也就是 test666。 ## 布隆过滤器的使用场景 - 解决Redis缓存穿透问题 - 邮件过滤,使用布隆过滤器来做邮件黑名单过滤 - 对爬虫网址进行过滤,爬过的不再爬 - 海量用户同时注册打满数据库情况 - 解决新闻推荐过的不再推荐(类似抖音刷过的往下滑动不再刷到) ## 布隆过滤器原理 BloomFilter 的算法是,首先分配一块内存空间做 bit 数组,数组的 bit 位初始值全部设为 0。 加入元素时,采用 k 个相互独立的 Hash 函数计算,然后将元素 Hash 映射的 K 个位置全部设置为 1。 检测 key 是否存在,仍然用这 k 个 Hash 函数计算出 k 个位置,如果位置全部为 1,则表明 key 存在,否则不存在。 <img src="https://pic.code-nav.cn/post_picture/1768274766864605186/hbV62DH8a2zf5Rgt.webp" alt="img" width="100%" />**哈希函数** 会出现碰撞,所以布隆过滤器会存在误判。 这里的误判率是指,BloomFilter 判断某个 key 存在,但它实际不存在的概率,因为它存的是 key 的 Hash 值,而非 key 的值。 所以有概率存在这样的 key,它们内容不同,但多次 Hash 后的 Hash 值都相同。 ## 代码中使用布隆过滤器 ### 引入Redisson依赖 ~~~xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> </dependency> ~~~ ### 配置Redis参数 ~~~yml spring: data: redis: host: 127.0.0.1 port: 6379 password: 123456 ~~~ ### 创建布隆过滤器实例 ~~~java @Configuration public class RBloomFilterConfiguration { /** * 防止用户注册查询数据库的布隆过滤器 */ @Bean public RBloomFilter<String> userRegisterCachePenetrationBloomFilter(RedissonClient redissonClient) { RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("userRegisterCachePenetrationBloomFilter"); cachePenetrationBloomFilter.tryInit(100000000, 0.001); return cachePenetrationBloomFilter; } } ~~~ tryInit 有两个核心参数: - expectedInsertions:预估布隆过滤器存储的元素长度。 - falseProbability:运行的误判率。 错误率越低,位数组越长,布隆过滤器的内存占用越大。 错误率越低,散列 Hash 函数越多,计算耗时较长。 我们可以通过这个在线网站进行占用内存大小对比。 <img src="https://pic.code-nav.cn/post_picture/1768274766864605186/Wb8HlRioTKu3nUTE.webp" alt="CleanShot 2024-08-28 at 13.50.05@2x" width="100%" /> 使用布隆过滤器的两种场景: 我们这里依然以用户注册为例 - 初始使用:注册用户时就向容器中新增数据,就不需要任务向容器存储数据了。 - 使用过程中引入:读取数据源将目标数据刷到布隆过滤器。 ### 在代码中引入 ~~~java @Service @RequiredArgsConstructor public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements UserService { private final RBloomFilter<String> userRegisterCachePenetrationBloomFilter; private final RedissonClient redissonClient; /** * 判断用户名是否存在 * * @param username 用户名 * @return true:存在;false:不存在 */ @Override public Boolean hasUsername(String username) { return userRegisterCachePenetrationBloomFilter.contains(username); } /** * 注册用户 * * @param requestParam 用户注册请求参数 */ @Override public void register(UserRegisterReqDTO requestParam) { // 判断用户名是否存在 if (hasUsername(requestParam.getUsername())) { throw new ClientException(UserErrorCodeEnum.USER_NAME__EXIST); } // 获取分布式锁 RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY + requestParam.getUsername()); try { if (lock.tryLock()) { // 保存用户信息 UserDO userDO = BeanUtil.convert(requestParam, UserDO.class); int insert = baseMapper.insert(userDO); if (insert != 1) { throw new ClientException(UserErrorCodeEnum.USER_SAVE_ERROR); } // 加入布隆过滤器 userRegisterCachePenetrationBloomFilter.add(requestParam.getUsername()); return; } throw new ClientException(UserErrorCodeEnum.USER_NAME__EXIST); } finally { lock.unlock(); } } } ~~~ **注意:** 这里有一个问题,当我们注册用户的时候,需要先通过布隆过滤器进行验证用户名是否存在,测试布隆过滤器中是没有添加用户名的,只有当我们将这个用户注册成功之后才会把用户添加到布隆过滤器中,所以这里会报异常。 **结局方案:** 我这里的做法是初始化初始化布隆过滤器 ~~~java @Slf4j @Component @RequiredArgsConstructor public class BloomFilterInitializer implements ApplicationRunner { private final UserMapper userMapper; private final RBloomFilter<String> userRegisterCachePenetrationBloomFilter; @Override public void run(ApplicationArguments args) throws Exception { log.info("开始加载数据库中的用户名到布隆过滤器..."); List<String> allUsernames = userMapper.getAllUsernames(); allUsernames.forEach(userRegisterCachePenetrationBloomFilter::add); log.info("加载完成,共计{}个用户名", allUsernames.size()); } } ~~~ 在系统启动时,将数据库中的所有用户名加载到布隆过滤器中。通过快速判断用户名是否存在来减少不必要的数据库查询。整个过程通过Spring的 `ApplicationRunner ` 接口在应用启动时自动触发。 大功告成! ## 其他几个功能注意点 ### 如何防止用户名重复? - **缓存策略**:将已有的用户名缓存到Redis中,在新用户注册时,先查询Redis缓存。如果用户名存在于缓存中,直接返回错误;如果不存在,再进行数据库检查和更新。 - **布隆过滤器**:结合布隆过滤器可以减少不必要的数据库查询,但最终还是需要数据库检查来保证唯一性。 通过布隆过滤器把所有用户名进行加载。这样该功能就能完全隔离数据库。然后在数据库层面添加唯一索引作为兜底。 ### 如何防止恶意请求毫秒级触发大量请求去一个未注册的用户名? 因为用户名没注册,所以布隆过滤器不存在,代表着可以触发注册流程插入数据库。但是如果恶意请求短时间海量请求,这些请求都会落到数据库,造成数据库访问压力。这里通过分布式锁,锁定用户名进行串行执行,防止恶意请求利用未注册用户名将请求打到数据库。 这里依旧以用户注册为例,使用布隆过滤器 + 分布式锁实现。 ~~~java @Override public void register(UserRegisterReqDTO requestParam) { // 判断用户名是否存在 if (hasUsername(requestParam.getUsername())) { throw new ClientException(UserErrorCodeEnum.USER_NAME__EXIST); } // 获取分布式锁 RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY + requestParam.getUsername()); try { if (lock.tryLock()) { // 保存用户信息 UserDO userDO = BeanUtil.convert(requestParam, UserDO.class); int insert = baseMapper.insert(userDO); if (insert != 1) { throw new ClientException(UserErrorCodeEnum.USER_SAVE_ERROR); } // 加入布隆过滤器 userRegisterCachePenetrationBloomFilter.add(requestParam.getUsername()); return; } throw new ClientException(UserErrorCodeEnum.USER_NAME__EXIST); } finally { lock.unlock(); } } } ~~~ 下面简述具体步骤: #### 步骤 1:检查布隆过滤器 1. 当一个注册请求到达时,首先通过布隆过滤器检查用户名是否存在。 2. 如果布隆过滤器判断用户名不存在,继续进入下一步。 3. 如果布隆过滤器判断用户名存在(即用户名可能已经注册),需要进一步确认数据库中的实际情况。 #### 步骤 2:获取分布式锁 1. 针对每个注册请求,获取基于用户名的分布式锁,确保同一时间只有一个请求能够处理该用户名的注册。 2. 如果锁获取失败(即该用户名正在被其他请求处理),可以返回一个提示信息或等待再重试。 #### 步骤 3:数据库验证 1. 在成功获取锁后,再次从数据库中确认用户名是否已存在。此步骤是为了处理并发情况下,布隆过滤器的假阳性问题。 2. 如果用户名已经存在,抛出异常,提示用户名已存在。 3. 如果用户名不存在,则进行用户数据的插入操作。 #### 步骤 4:释放锁 1. 无论注册操作成功与否,都要确保在 `finally` 块中释放分布式锁,防止死锁的情况发生。 ### 如果恶意请求全部使用未注册的用户名发起注册该怎么办? 结论:系统无法进行完全风控,只有通过类似于限流的功能进行保障系统安全。 如果恶意请求全部使用已经注册的用户名发起注册请求,这种情况可能会导致系统频繁地进行数据库查询、锁定和资源消耗。 当然我们可以通过其他手段对我们的服务端进行防护: #### 1. **速率限制(Rate Limiting)** - 对于同一用户名或同一IP的注册请求,设置速率限制。每个用户名在一定时间内只能发起有限次数的注册请求,一旦超过限制,系统可以返回错误或阻止请求。 - 可以使用工具如 `RateLimiter`、Nginx 的 `limit_req` 模块,或在 API 网关层实现速率限制。 #### 2. 使用Sentinal进行限流 Sentinel 提供了多种限流策略,可以精确控制流量: - **固定窗口限流**:按照固定的时间窗口限制流量,比如每秒允许的最大请求数。 - **滑动窗口限流**:基于滑动窗口的限流策略,更平滑地处理流量。 - **漏桶算法**:确保请求以固定速率处理,避免突发流量的影响。 - **令牌桶算法**:允许在短时间内处理突发流量,但限制长期的请求速率。 #### 3. 分布式缓存检查 在布隆过滤器的基础上,可以结合分布式缓存(如 Redis),在内存中缓存最近频繁访问的用户名。对于这些用户名,系统可以快速响应,不进行重复的数据库查询。

假如你负责一个并发量很高的系统,现在出现了比较复杂的数据不一致问题?你认为会存在哪些可能?

整理一下昨天的题目: 假如你负责一个并发量很高的系统,现在出现了比较复杂的数据不一致问题?你认为会存在哪些可能? 只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。 但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。 **解决方案如下**: 更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新执行“读取数据+更新缓存”的操作,根据唯一标识路由之后,也发送到同一个 jvm 内部队列中。 一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。 这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。 待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。 如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。 高并发的场景下,该解决方案要注意的问题: **读请求长时阻塞** 由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。 该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。 另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每个库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致读请求的长时阻塞。 一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。 如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。 其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。 我们来实际粗略测算一下。 如果一秒有 500 的写操作,如果分成 5 个时间片,每 200ms 就 100 个写操作,放到 20 个内存队列中,每个内存队列,可能就积压 5 个写操作。每个写操作性能测试后,一般是在 20ms 左右就完成,那么针对每个内存队列的数据的读请求,也就最多 hang 一会儿,200ms 以内肯定能返回了。 经过刚才简单的测算,我们知道,单机支撑的写 QPS 在几百是没问题的,如果写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每个机器 20 个队列。 读请求并发量过高 这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。 但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。 **多服务实例部署的请求路由** 可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。 比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。 **热点商品的路由问题,导致请求的倾斜** 万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。

鱼皮您好,我现在工作上遇见了了

鱼皮您好,我现在工作上遇见了了一个问题,一个2.5亿的表,要批量修改一批四百万的数据的状态,由于该状态字段没有索引,现在初步想法想分批进行,数据库使用的是MySQL,还有就是分批的话一批改多少条比较稳妥,可以现在没有IO的时候进行,但时间不能超过两小时,可以多次,您有什么好的意见吗不吝赐教谢谢(。ò ∀ ó。)

鱼皮你好,想问下关于文件上传的

鱼皮你好,想问下关于文件上传的几个问题。 1.文件上传要求固定格式,比如要求表格格式(必须三列)。那么文件内容的校验是在前端做还是后端做合适,或者是不是小文件前端校验,大文件后端校验? 2.如果在后端校验,那我有一个这样的业务需求。第一次上传需要后端将文件的三个列的前n行返回,来做个文件内容demo给用户看,同时用户决定最终存储文件的哪几列。那么当用户决定了以后,是再一次上传这个文件给后端,还是后端存下第一次上传的文件,第二次上传时前端只给个上传命令。 3.存的话存在哪里合适呢,假如文件大小我们控制不了。 4.如果真的是两次上传,那怎么保证两次上传文件的一致性呢? 希望我能表述明白我的问题,这个真的困扰我很久了,感谢鱼皮!!!

鱼皮你好,想问下关于文件上传的

鱼皮你好,想问下关于文件上传的几个问题。 1.文件上传要求固定格式,比如要求表格格式(必须三列)。那么文件内容的校验是在前端做还是后端做合适,或者是不是小文件前端校验,大文件后端校验? 2.如果在后端校验,那我有一个这样的业务需求。第一次上传需要后端将文件的三个列的前n行返回,来做个文件内容demo给用户看,同时用户决定最终存储文件的哪几列。那么当用户决定了以后,是再一次上传这个文件给后端,还是后端存下第一次上传的文件,第二次上传时前端只给个上传命令。 3.存的话存在哪里合适呢,假如文件大小我们控制不了。 4.如果真的是两次上传,那怎么保证两次上传文件的一致性呢? 希望我能表述明白我的问题,这个真的困扰我很久了,感谢鱼皮!!!

qiankun?可能这些微前端框架更适合你

<html> <head></head> <body> <div class="content ql-editor"> <p>大家好,我是多喝热水。</p> <p>最近咱们<a href="https://codefather.cn" target="_blank">编程导航</a>网站需要去集成一些工具库,而这些工具又是一些独立存在的个体且技术栈不同,我们在想如何能够以最低成本去接入这些工具的功能,因此我们调研了一些主流的微前端框架并进行了实践。</p> <p>本文将深入探讨微前端应用的架构设计,分析社区中主流的微前端解决方案,并通过实际案例演示如何跑通一个微前端应用~</p> <h2>一、微前端应用的架构?</h2> <h3>主应用</h3> <p>微前端需要一个主应用,它负责去调度不同的微应用,相当于是一个应用基座。</p> <h3>微应用</h3> <p>微应用可以有多个,可以是不同技术栈开发的项目,如 Vue.js、React.js、Solid.js 等。</p> <h2>二、社区微前端的解决方案</h2> <h3>1、无界</h3> <p>腾讯在维护的,后起之秀,非常轻量,3k star,无痛接入 vite,对比下来个人比较喜欢的一种微前端方案。</p> <h4>无界的运行模式</h4> <p>此图为无界官方文档的运行模式图</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/ov8eia94.jpeg" alt="file_1710405791889_432.png"></p> <h4>跑通一个案例</h4> <p>1)第一步安装主应用基座,执行如下图命令:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/fuki9zhn.jpeg" alt="image.png"></p> <p></p> <p>2)第二步初始化package.json文件并基于 pnpm 配置 monorepo 架构(<strong>详细文档移步</strong><a href="https://yuyuanweb.yuque.com/org-wiki-yuyuanweb-zvq1bg/csfyxv/zfrqgutodzwvo4pm" title="从理解软/硬链接掌握PNPM" target="_blank">从理解软/硬链接掌握PNPM</a>),如下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/ef4ql22u.jpeg" alt="image.png"><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/phdl8q1o.jpeg" alt=""></p> <p></p> <p>3)编写 pnpm-workspace.yaml 文件,如下:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> packages: - <span class="ql-token hljs-string">'main'</span> # main目录下的packge.json需要安装 - <span class="ql-token hljs-string">'packages/**'</span> #packages目录下的所有项目的<span class="ql-token hljs-keyword">package</span>.json需要安装 </div> </div> <p></p> <p>4)创建多个微应用,如下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/b7omnu1l.jpeg" alt="image.png"></p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/451ymio3.jpeg" alt="image.png"></p> <p></p> <p>4)安装无界并将所有微应用关联起来</p> <p></p> <p>无界对于Nodejs的版本有要求,需要Nodejs &gt; v18,参考此处文档<a href="https://wujie-micro.github.io/doc/guide/install.html#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B" target="_blank">无界</a></p> <p></p> <p>在主应用安装wujie-vue3(同时会把我们主/微应用所需要的所有依赖的都安装完),这个是作者封装的组件,可以减少用户配置的成本,如下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/ee276qro.jpeg" alt="image.png"></p> <p></p> <p>此时我们的目录是这样的,node_modules中只保留最核心的包,一些辅助用的包,如Babel会被提升到最外层的node_modules,如下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/08sdxmpf.jpeg" alt="image.png"></p> <p></p> <p>5)在主应用中注册无界组件,如下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/ff1mmg1t.jpeg" alt="image.png"></p> <p></p> <p>6)启动主应用和所有微应用</p> <p></p> <p>这里我们可以给最外层的package.json配置一下scripts脚本,避免每次都需要进入packages文件夹启动微应用,配置完成依次启动,如下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/a8fesz2o.jpeg" alt="image.png"></p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/ky4o844n.jpeg" alt="image.png"></p> <p></p> <p>7)启动后在主应用中引入对应的微应用</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/l5yor1c3.jpeg" alt="image.png"></p> <p></p> <p>8)完成启动</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/mrxmgpus.jpeg" alt="image.png"></p> <p></p> <h4>总结</h4> <p></p> <p>从跑通这个案例来看,无界的接入非常简单轻量,且完全支持 vite(这是其他微前端框架没有的)</p> <p></p> <h4>简单了解一下原理</h4> <p></p> <p>1)<strong>CSS 隔离</strong>:使用 shadowDOM 隔离</p> <p></p> <p>2)<strong>JS 隔离</strong>:使用一个空的 iFrame 隔离</p> <p></p> <p>3)<strong>多应用通讯</strong>:使用 Proxy</p> <p></p> <h3>2、qiankun</h3> <p></p> <p>该框架是蚂蚁在维护的,15k star,目前官方使用的是 webpack 作为构建工具,没有明确表示支持 vite,社区有 vite-plugin-qiankun 插件支持,但是配置起来还是有一定的心智负担,且可能存在不确定的问题,故此处使用webpack进行演示。</p> <p></p> <h4>跑通一个案例</h4> <p></p> <p>1)创建主应用(这次我们使用create-react-app来创建基于react的主应用)</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/s3lj7all.jpeg" alt="image.png"></p> <p></p> <p>2)配置monorepo架构(与无界中一致,这里不再赘述)</p> <p></p> <p>3)创建微应用(这里我们统一都使用 webpack 作为构建工具)</p> <p></p> <p>首先我们安装一下 vue/cli,如下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/8syddp9t.jpeg" alt="image.png"></p> <p></p> <p>再创建一个基于vue/cli的微应用,如下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/r3mf4odt.jpeg" alt="image.png"></p> <p></p> <p>4)在主应用中注册微应用</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { registerMicroApps, start } from <span class="ql-token hljs-string">'qiankun'</span>; registerMicroApps([ { name: <span class="ql-token hljs-string">'vue app'</span>, entry: <span class="ql-token hljs-string">'//localhost:7100'</span>, container: <span class="ql-token hljs-string">'#vueapp'</span>, activeRule: <span class="ql-token hljs-string">'/vueapp'</span>, }, ]); start(); </div> </div> <p></p> <p>在主应用中配置微应用的显示位置,如下:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/ryral2d6.jpeg" alt="image.png"></p> <p></p> <p>5)配置微应用,根据文档所述,我们需要在微应用的入口导出对应的生命周期钩子给到qiankun在合适的时机调用,且需要根据不同的运行时设置不同的publicPath,参考此处文档 <a href="https://qiankun.umijs.org/zh/guide/tutorial#vue-%E5%BE%AE%E5%BA%94%E7%94%A8" target="_blank">项目实践 - qiankun</a></p> <p></p> <p>第一步,在微应用入口文件的同级目录下创建一个public-path.js文件,并写入如下内容:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">if</span> (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } </div> </div> <p></p> <p>第二步,在微应用的package.json中加入如下声明,告诉 eslint 这个 <strong>webpack_public_path</strong> 全局变量时存在的,不要抛出错误,如下:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-string">"eslintConfig"</span>: { ... <span class="ql-token hljs-string">"globals"</span>: { <span class="ql-token hljs-string">"__webpack_public_path__"</span>: <span class="ql-token hljs-literal">true</span> }, ... }, </div> </div> <p></p> <p>第三步,在微应用入口文件抛出生命周期钩子函数给到qiankun调用,如下:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> <span class="ql-token hljs-string">"./public-path"</span>; <span class="ql-token hljs-keyword">import</span> { createApp } from <span class="ql-token hljs-string">"vue"</span>; <span class="ql-token hljs-keyword">import</span> App from <span class="ql-token hljs-string">"./App.vue"</span>; <span class="ql-token hljs-type">let</span> <span class="ql-token hljs-variable">instance</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-literal">null</span>; function <span class="ql-token hljs-title">render(props = {})</span> { const { container } = props; createApp(App).mount(container ? container.querySelector(<span class="ql-token hljs-string">"#app"</span>) : <span class="ql-token hljs-string">"#app"</span>); } <span class="ql-token hljs-comment">// 独立运行时</span> <span class="ql-token hljs-keyword">if</span> (!window.__POWERED_BY_QIANKUN__) { render(); } export async function <span class="ql-token hljs-title">bootstrap()</span> { console.log(<span class="ql-token hljs-string">"[vue] vue app bootstraped"</span>); } export async function <span class="ql-token hljs-title">mount(props)</span> { console.log(<span class="ql-token hljs-string">"[vue] props from main framework"</span>, props); render(props); } export async function <span class="ql-token hljs-title">unmount()</span> { instance?.unmount(); instance.$el.innerHTML = <span class="ql-token hljs-string">""</span>; instance = <span class="ql-token hljs-literal">null</span>; } </div> </div> <p></p> <p>第四步,在vue.config.js文件中配置微应用的打包方式,并配置跨域【重要】</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> const { defineConfig } = require(<span class="ql-token hljs-string">"@vue/cli-service"</span>); const { name } = require(<span class="ql-token hljs-string">"./package"</span>); <span class="ql-token hljs-keyword">module</span>.<span class="ql-token hljs-keyword">exports</span> = defineConfig({ transpileDependencies: <span class="ql-token hljs-literal">true</span>, devServer: { headers: { <span class="ql-token hljs-string">"Access-Control-Allow-Origin"</span>: <span class="ql-token hljs-string">"*"</span>, }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: <span class="ql-token hljs-string">"umd"</span>, <span class="ql-token hljs-comment">// 把微应用打包成 umd 库格式</span> chunkLoadingGlobal: `webpackJsonp_${name}`, <span class="ql-token hljs-comment">// webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal</span> }, }, }); </div> </div> <p></p> <p>6)启动微应用和主应用</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/i2kkyujp.jpeg" alt="image.png"></p> <p></p> <p>7)访问/vue-app 路由成功渲染微应用</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/l0ltaqdk.jpeg" alt="image.png"></p> <p></p> <h4>总结</h4> <p></p> <p>1)配置比较繁琐,个人感觉需要用户去配置的东西太多了,单从启动一个微前端应用来说,确实没有无界方便</p> <p></p> <p>2)暂时无法支持 vite 作为开发服务器,只能使用 webpack,接入相对比较重</p> <p></p> <h3>3、两者实现原理对比(拓展)</h3> <p></p> <p>参考文章:<a href="https://juejin.cn/post/7221366548077707321" target="_blank">https://juejin.cn/post/7221366548077707321</a></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/xt2omq9q.jpeg"></p> <p></p> <h3>4、两者优缺点对比</h3> <p><br></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/hx6hbizo.jpeg"></p> <p></p> <h2>三、对微前端的一些看法</h2> <p></p> <h3>什么时候需要用到它?</h3> <p></p> <p>1)不同项目使用的技术栈不同,想将它们组合到一起</p> <p></p> <p>2)为了解决项目代码组织问题,而不是性能问题</p> <p></p> <h3>一个使用场景</h3> <p></p> <p>假如我有一个系统是用 jQuery + PHP 写的,但是它已经在线上稳定运行了好几年的时间,而现在需要在这个系统上加一些新需求,但是碍于技术栈过于老旧而导致开发效率低下,想使用新的技术栈来开发(如Vue.js、React.js),重构又浪费时间和人力,那这个时候就可以考虑接入微前端,把旧系统作为一个微应用,新的业务逻辑重开一个项目写(也是一个微应用),这样我们使用最小的成本提升了开发效率。</p> <p></p> <h3>优点</h3> <p></p> <p>1)老旧技术栈项目也能<strong>平滑迁移</strong>至新的技术栈,提升项目的可扩展性</p> <p></p> <p>2)主应用<strong>不限制接入应用的技术栈</strong>,微应用可以自主选择技术栈</p> <p></p> <p>3)独立部署,降低一个前端应用每次部署涉及的范围,一定程度上减少了项目风险</p> <p></p> <h3>缺点</h3> <p></p> <p>1)应用拆分的越小,架构就会变得复杂、维护成本就会变高</p> <p></p> <p>2)技术栈多样化,同时也意味着技术栈混乱(但一般也不会超过2种以上?)</p> <p></p> <h2>四、其他补充</h2> <p></p> <h3>micro-app</h3> <p></p> <p>star 5k,也是一个微前端框架,京东推出,micro-app 的接入和wujie的步骤一致,<strong>仅仅只是将 wujie-vue3 换成了 @micro-zoe/micro-app</strong></p> <p></p> <p>如下所示:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { createApp } from <span class="ql-token hljs-string">"vue"</span>; <span class="ql-token hljs-keyword">import</span> <span class="ql-token hljs-string">"./style.css"</span>; <span class="ql-token hljs-keyword">import</span> App from <span class="ql-token hljs-string">"./App.vue"</span>; <span class="ql-token hljs-keyword">import</span> microApp from <span class="ql-token hljs-string">"@micro-zoe/micro-app"</span>; createApp(App).mount(<span class="ql-token hljs-string">"#app"</span>); microApp.start(); </div> </div> <p></p> <p><strong>注意</strong>:在主应用中如果子应用是采用vite接入,那么需要使用 iframe, <a href="https://micro-zoe.github.io/micro-app/docs.html#/zh-cn/framework/vite" target="_blank">官方的解释</a></p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> &lt;template&gt; &lt;micro-app url=<span class="ql-token hljs-string">"http://localhost:5173"</span> height=<span class="ql-token hljs-string">"500px"</span> name=<span class="ql-token hljs-string">"vue-app"</span> iframe&gt;&lt;/micro-app&gt; &lt;micro-app url=<span class="ql-token hljs-string">"http://localhost:5174"</span> height=<span class="ql-token hljs-string">"500px"</span> name=<span class="ql-token hljs-string">"react-app"</span> iframe&gt;&lt;/micro-app&gt; &lt;/template&gt; </div> </div> <p></p> <p>否则你可能会看到如下错误:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1732240055109545986/1tdkfx3k.jpeg" alt="image.png"></p> <p></p> <h2>五、最后</h2> <p></p> <p>大家还知道有哪些微前端框架可以在评论区讨论~</p> <p>#前端# #技术# #系统设计#</p> </div> </body> </html>

羽哥,我遇到一个项目,就是做一

羽哥,我遇到一个项目,就是做一个网页端的办公区室内地图,并与后台进行数据交互,实时显示哪些会议室空闲,空闲的会议室就呈现绿色,使用中的会议室呈现绿色,就类似于这个公厕系统的功能,我主要的疑问是该怎么把这个办公区地图这些条条框框绘制到网页上?该用什么技术?要用SVG代码绘制吗?有简单的方法吗?

yes哥您好,我想问一个系统设

yes哥您好,我想问一个系统设计题。用户注册场景 海数据时怎么快速判断 username唯一。比如我们注册谷歌账号时,输入用户名,迅速就能判断出来该名称有没有被注册过。但是谷歌用户几十亿级别 是怎么迅速判断出来的呢?难道全表扫描user表?就算加了缓存也得全部扫描缓存? 我大概想到的难点有3个:1.字符串怎么匹配(比如kmp);2.数据怎么存储【重点问题1】;3.几十亿数据怎么提出来,海量的数据怎么加载到内存让cpu去计算?【重点问题2】 另一种思路:采用某种数据结构优化?【重点问题3】 希望您能给我讲讲这个系统设计的思路,以及三个重点问题的解决方案,谢谢!

什么数据结构能够提高存储、查询的优化效果?

鱼皮您好,想咨询您一个功能问题。需要在网关存储IP的黑灰名单(IPV4),预计一共2000万左右,要求存储和查询都较优,存储在JVM堆外内存中,采用什么数据结构比较好能达到要求呢?

鱼皮有时间可以讲一下 1024

鱼皮有时间可以讲一下 1024code.com的这个网站 他的这个在线debug是如何做的吗

下载 APP