谷粒商城【成神路】-【10】——缓存

news2024/11/17 2:30:11

目录

🧂1.引入缓存的优势

🥓2.哪些数据适合放入缓存 

🌭3.使用redis作为缓存组件 

🍿4.redis存在的问题 

🧈5.添加本地锁 

🥞6.添加分布式锁

🥚7.整合redisson作为分布式锁


🚗🚗🚗1.引入缓存的优势

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落盘工作。

🚗🚗🚗2.哪些数据适合放入缓存 

  • 即时性、数据—致性要求不高的
  • 访问量大且更新频率不高的数据(读多,写少)

🚗🚗🚗3.使用redis作为缓存组件 

先确保reidis正常启动

3.1配置redis

  • 1.引入依赖
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  • 2.配置reids信息
spring
  redis:
    port: 6379
    host: ip地址
    password: XXX

3.2优化查询 

之前都是从数据库查询的,现在加入缓存逻辑~

 /**
     * 使用redis缓存
     */
    @Override
    public Map<String, List<Catalog2Vo>> getCatalogJson() {

        //1.加入缓存,缓存中存的数据全都是json
        String catalogJson = redisTemplate.opsForValue().get("catalogJson");
        if (StringUtils.isEmpty(catalogJson)) {
            //2.缓存中如果没有,再去数据库查找
            Map<String, List<Catalog2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
            //3.将数据库查到的数据,将对象转换为json存放到缓存
            String s = JSON.toJSONString(catalogJsonFromDB);
            redisTemplate.opsForValue().set("catalogJson", s);
        }
        //4.从缓存中获取,转换为我们指定的类型
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {
        });

        return result;
    }

    /**
     * 从数据库查询并封装的分类数据
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDB() {

        /**
         * 优化:将数据库查询的多次变为一次
         */

        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1.查出所有1级分类
        List<CategoryEntity> leve1Categorys = getParent_cid(selectList, 0L);

        //2.封装数据
        Map<String, List<Catalog2Vo>> parentCid = leve1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1.每一个一级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
            List<Catalog2Vo> catalog2Vos = null;
            if (categoryEntities != null) {
                catalog2Vos = categoryEntities.stream().map(l2 -> {
                    Catalog2Vo vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //找二级分类的三级分类
                    List<CategoryEntity> categoryEntities3 = getParent_cid(selectList, l2.getCatId());
                    if (categoryEntities3 != null) {
                        List<Catalog2Vo.Catalog3Vo> collect = categoryEntities3.stream().map(l3 -> {
                            Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());
                        vo.setCatalog3List(collect);
                    }
                    return vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));

        return parentCid;
    }

3.3测试 

在本地第一次查询后,查看redis,发现redis已经存储

使用JMeter压测一下

🚗🚗🚗4.redis存在的问题 

4.1缓存穿透

  • 缓存穿透:  查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
  • 解决方案:null结果缓存,并加入短暂过期时间

4.2缓存雪崩 

  • 缓存雪崩: 在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,大量请求全部转发到DB, DB瞬时压力过重雪崩。
  • 解决方案:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

4.3缓存击穿 

缓存击穿 :对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

解决方案:枷锁    大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

🚗🚗🚗5.添加本地锁 

若在缓存redis中没有查到,也去数据库查询,在查询数据库时,添加本地锁,在查之前,再次判断缓存reids中是否有数据,如果有,直接返回,如果没有,在查数据库,在查完数据库后,由于延迟原因,我们查完数据库时,将数据存放到缓存中,然后在释放锁

/**
     * 使用redis缓存
     */
    @Override
    public Map<String, List<Catalog2Vo>> getCatalogJson() {
        //1.使用redis缓存,存储为json对象
        String catalogJson = redisTemplate.opsForValue().get("catalogJson");
        //2.判断缓存中是否有
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存没有命中~查询数据库...");
            //3.如果缓存中没有,从数据库
            Map<String, List<Catalog2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
            return catalogJsonFromDB;
        }
        System.out.println("缓存命中....直接返回");
        //5.如果缓存中有,转换为我们需要的类型
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {
        });
        return result;
    }


    /**
     * 从数据库查询并封装的分类数据
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDB() {

        /**
         * 优化:将数据库查询的多次变为一次
         */

        //TODO 本地锁,在分布式下,必须使用分布式锁
        //加锁,防止缓存击穿,使用同一把锁
        synchronized (this) {

            //加所以后,我们还要去缓存中确定一次,如果缓存中没有,才继续查数据库
            String catalogJson = redisTemplate.opsForValue().get("catalogJson");
            if (!StringUtils.isEmpty(catalogJson)) {
                //如果缓存中有,从缓存中获取
                Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {
                });
                return result;
            }
            System.out.println("查询了数据库~");

            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1.查出所有1级分类
            List<CategoryEntity> leve1Categorys = getParent_cid(selectList, 0L);

            //2.封装数据
            Map<String, List<Catalog2Vo>> parentCid = leve1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //1.每一个一级分类
                List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
                List<Catalog2Vo> catalog2Vos = null;
                if (categoryEntities != null) {
                    catalog2Vos = categoryEntities.stream().map(l2 -> {
                        Catalog2Vo vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                        //找二级分类的三级分类
                        List<CategoryEntity> categoryEntities3 = getParent_cid(selectList, l2.getCatId());
                        if (categoryEntities3 != null) {
                            List<Catalog2Vo.Catalog3Vo> collect = categoryEntities3.stream().map(l3 -> {
                                Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                                return catalog3Vo;
                            }).collect(Collectors.toList());
                            vo.setCatalog3List(collect);
                        }
                        return vo;
                    }).collect(Collectors.toList());
                }
                return catalog2Vos;
            }));
            //4.将从数据库中获取的数据,转换为Json存储到redis
            String s = JSON.toJSONString(parentCid);
            //设置缓存时间,方式雪崩
            redisTemplate.opsForValue().set("catalogJson", s, 1, TimeUnit.DAYS);
            return parentCid;
        }
    }

🚗🚗🚗6.添加分布式锁

使用分布式锁 步骤

  • 1.先去redis中设置一个key,为了保持原子性,同时设置过期时间
  • 2.判断是否设置成功,成功则继续业务操作,失败则自选再次获取
  • 3.执行业务之后,需要删除key释放锁,为了保持原子性,使用lua脚本
 /**
     * 使用redis缓存
     */
    @Override
    public Map<String, List<Catalog2Vo>> getCatalogJson() {
        //1.使用redis缓存,存储为json对象
        String catalogJson = redisTemplate.opsForValue().get("catalogJson");
        //2.判断缓存中是否有
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存没有命中~查询数据库...");
            //3.如果缓存中没有,从数据库
            Map<String, List<Catalog2Vo>> catalogJsonFromDB = getCatalogJsonFromDBWithRedisLock();
            return catalogJsonFromDB;
        }
        System.out.println("缓存命中....直接返回");
        //5.如果缓存中有,转换为我们需要的类型
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {
        });
        return result;
    }


    /**
     * 从数据库中获取数据,使用分布所锁
     *
     * @return
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDBWithRedisLock() {
        //1.占分布式锁,去redis占位
        String uuid = UUID.randomUUID().toString();
        //2.设置过期时间,必须和加锁同步
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            System.out.println("获取分布式锁成功..." + redisTemplate.opsForValue().get("lock"));
            //加锁成功...执行业务
            Map<String, List<Catalog2Vo>> dataFromDb;
            try {
                dataFromDb = getDataFromDb();
            } finally {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                //删除锁,原子性
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return getDataFromDb();
        } else {
            //加锁失败...重试
            //休眠100毫秒
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {

            }
            return getCatalogJsonFromDBWithRedisLock();//自旋方式
        }
    }




    /**
     * 提起方法,从数据库中获取
     *
     * @return
     */
    private Map<String, List<Catalog2Vo>> getDataFromDb() {
        //加所以后,我们还要去缓存中确定一次,如果缓存中没有,才继续查数据库
        String catalogJson = redisTemplate.opsForValue().get("catalogJson");
        if (!StringUtils.isEmpty(catalogJson)) {
            //如果缓存中有,从缓存中获取
            Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {
            });
            return result;
        }
        System.out.println("查询了数据库~");

        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1.查出所有1级分类
        List<CategoryEntity> leve1Categorys = getParent_cid(selectList, 0L);

        //2.封装数据
        Map<String, List<Catalog2Vo>> parentCid = leve1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1.每一个一级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
            List<Catalog2Vo> catalog2Vos = null;
            if (categoryEntities != null) {
                catalog2Vos = categoryEntities.stream().map(l2 -> {
                    Catalog2Vo vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //找二级分类的三级分类
                    List<CategoryEntity> categoryEntities3 = getParent_cid(selectList, l2.getCatId());
                    if (categoryEntities3 != null) {
                        List<Catalog2Vo.Catalog3Vo> collect = categoryEntities3.stream().map(l3 -> {
                            Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());
                        vo.setCatalog3List(collect);
                    }
                    return vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
        //4.将从数据库中获取的数据,转换为Json存储到redis
        String s = JSON.toJSONString(parentCid);
        //设置缓存时间,方式雪崩
        redisTemplate.opsForValue().set("catalogJson", s, 1, TimeUnit.DAYS);
        return parentCid;
    }

🚗🚗🚗7.整合redisson作为分布式锁

7.1引入依赖

       <!--redisson作为所有分布式锁-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>

7.2程序化配置

在配置地址时,一定要添加reds://

@Configuration
public class MyRedissonConfig {


    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() throws IOException {
        //1.创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.20.130:6379");
        //2.根据config创建redisson实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

7.3实例解析

  • 1.锁的自动续期,如果业务超长,运行期间自动给锁续上新的30秒,不用担心业务超长,所自动过期被删除掉
  • 2.加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30秒后自动删除
 @Autowired
 RedissonClient redissonClient;

 @ResponseBody
    @GetMapping("/hello")
    public String hello() {
        //1.设置redis的key获取一把锁,只要名字相同,就是同一把锁
        RLock lock = redissonClient.getLock("my-lock");
        //2.手动枷锁
        lock.lock();//阻塞式等待,默认30秒
        try {
            //3.执行业务
            System.out.println("枷锁成功!" + Thread.currentThread().getName());
            //4.模拟业务消耗时间
            Thread.sleep(20000);
        } catch (Exception e) {

        } finally {
            //3.释放锁
            System.out.println("释放锁~"+Thread.currentThread().getName());
            lock.unlock();//不删除,默认30秒后过期,自动删除
        }
        return "hello";
    }
  • 3.如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
  • 4.如果未指定锁的超时时间,就使用30*1000【LockWatchingTimeOut看门狗默认时间】 
    • 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔(【LockWatchingTimeOut看门狗默认时间】/3)折磨长时间自动续期;

7.4读写锁 

  • 1.保证一定能读到最新数据,修改期间,写锁是一个排他锁,读锁是一个共享锁
  • 2.读+读:相当于并发,在redis中记录好,都会读取成功
  • 3.写+读:等待写锁释放
  • 4.写+写:阻塞方式
  • 5.读+写:有读锁,写也需要等待
 @GetMapping("/write")
    @ResponseBody
    public String write() {

        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWrite-lock");
        String word = "";
        RLock rLock = readWriteLock.writeLock();
        try {
            //该数据加写锁,读数据加读锁
            rLock.lock();
            word = UUID.randomUUID().toString();
            Thread.sleep(3000);
            redisTemplate.opsForValue().set("writeValue", word);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return word;
    }


    /**
     * 读写锁
     * @return
     */
    @GetMapping("/read")
    @ResponseBody
    public String read() {
        String word = "";
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWrite-lock");
        //加读锁
        RLock rLock = readWriteLock.readLock();
        rLock.lock();
        try {
            word = redisTemplate.opsForValue().get("writeValue");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            rLock.unlock();
        }
        return word;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1508459.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript实现通过键盘弹钢琴的效果

本片文章通过触发键盘事件来触发对应的音乐&#xff0c;而且给页面添加了渐变的active类名&#xff0c;通过触发不同的鼠标事件&#xff0c;然后active类移动来实现按下钢琴键的视觉效果。 关键代码&#xff1a; <!DOCTYPE html> <html lang"en"><h…

提示并输入一个字符串,统计该字符中大写、小写字母个数、数字个数、空格个数以及其他字符个数要求使用C++风格字符串完成

#include <iostream> #include <array> using namespace std;int main() {cout<<"请输入一个字符串"<<endl;//array<string,100> str;string str;getline(cin,str);int daxie0,xiaoxie0,num0,space0,other0;int lenstr.size();;for(in…

Java随手记

equals和的区别 使用基本数据类型&#xff08;char&#xff0c;int&#xff0c;long等&#xff09;的时候&#xff0c;比较的是它们的值 使用引用数据类型的时候&#xff0c;比较的是地址 equals如果不重写&#xff0c;那么和 是没差别的 下面来看String的比较&#xff0c;这…

Spring Security自定义认证授权过滤器

自定义认证授权过滤器 自定义认证授权过滤器1、SpringSecurity内置认证流程2、自定义Security认证过滤器2.1 自定义认证过滤器2.2 定义获取用户详情服务bean2.3 定义SecurityConfig类2.4 自定义认证流程测试 3、 基于JWT实现无状态认证3.1 认证成功响应JWT实现3.2 SpringSecuri…

Kafka MQ 生产者和消费者

Kafka MQ 生产者和消费者 Kafka 的客户端就是 Kafka 系统的用户&#xff0c;它们被分为两种基本类型:生产者和消费者。除 此之外&#xff0c;还有其他高级客户端 API——用于数据集成的 Kafka Connect API 和用于流式处理 的 Kafka Streams。这些高级客户端 API 使用生产者和消…

如何保证消息的可靠传输

数据的丢失问题&#xff0c;可能出现在生产者、MQ、消费者中 生产者丢失&#xff1a; 生产者将数据发送到 RabbitMQ 的时候&#xff0c;可能数据就在半路给搞丢了&#xff0c;因为网络问题啥的&#xff0c;都有可能。此时可以选择用 RabbitMQ 提供的事务功能&#xff0c;就是生…

脚手架cli快速创建Vue2/Vue3项目

前言&#xff1a; 本文的nodejs版本是14.21.3 第一步 进入cmd窗口 1、全局安装webpack npm install webpack-g&#xff0c; npm install webpack-g 第二步 2、全局安装vue脚手架 npm install -g vue/cli 第三步 3、初始化vue项目 &#xff08;vue脚手架使用webpack模…

资料下载-嵌入式 Linux 入门

学习的第一步是去下载资料。 1. 有哪些资料 所有资料分 4 类&#xff1a; ① 开发板配套资料(原理图、虚拟机的映像文件、烧写工具等)&#xff0c;放在百度网盘 ② 录制视频过程中编写的文档、源码、图片&#xff0c;放在 GIT 仓库 ③ u-boot、linux 内核、buildroot 等比较大…

STM32CubeMX学习笔记18——FSMC(TFT-LCD屏触摸)

1.触摸屏简介 目前最常用的触摸屏有两种&#xff1a;电阻式触摸屏和电容式触摸屏 1.1 电阻式触摸屏 电阻式的触摸屏结构如下图示&#xff0c;它主要由表面硬涂层、两个ITO层、间隔点以及玻璃底层构成&#xff0c;这些结构层都是透明的&#xff0c;整个触摸屏覆盖在液晶面板上…

45、C++/基础练习20240311

一、提示并输入一个字符串&#xff0c;统计该字符中大写、小写字母个数、数字个数、空格个数以及其他字符个数 要求 使用C风格字符串完成。 代码&#xff1a; #include <iostream>using namespace std;int main() {string buf;//定义字符串类型变量bufcout << &…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Stack容器组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之Stack容器组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、Stack容器组件 堆叠容器&#xff0c;子组件按照顺序依次入栈&#xff0c;后一…

软件测试需要学什么?学多久?软件测试技术进阶路线图

很多新手&#xff0c;不知道软件测试学习该如何开始&#xff0c;软件测试需要掌握哪些知识。下面是根据本人的理解&#xff0c;粗略整理的一个学习大纲&#xff0c;基本上涵盖了软件测试工程师需要掌握的全部技能&#xff0c;希望对刚入行或者准备学习测试的朋友提供一点指引。…

哈希表|1.两数之和

力扣题目链接 /*** Note: The returned array must be malloced, assume caller calls free().*/// leetcode 支持 ut_hash 函式庫typedef struct {int key;int value;UT_hash_handle hh; // make this structure hashable} map;map* hashMap NULL;void hashMapAdd(int key, i…

Qt教程 — 1.1 Linux下安装Qt

目录 1 下载Qt 1.1 官方下载 1.2 百度网盘下载 1.3 Linux虚拟机终端下载 2 Qt安装 3 安装相关依赖 4 测试安装 1 下载Qt 1.1 官方下载 通过官网下载对应版本&#xff0c;本文选择的版本为qt-opensource-linux-x64-5.12.12&#xff0c;Qt官方下载链接&#xff1a;htt…

【C++庖丁解牛】模拟实现STL的string容器(最后附源码)

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1.vs和g下string结构…

P5266 【深基17.例6】学籍管理题解

题目 您要设计一个学籍管理系统&#xff0c;最开始学籍数据是空的&#xff0c;然后该系统能够支持下面的操作&#xff08;不超过条&#xff09;&#xff1a; 插入与修改&#xff0c;格式1 NAME SCORE&#xff1a;在系统中插入姓名为NAME(由字母和数字组成不超过20个字符的字符…

如何使用群晖NAS结合cpolar内网穿透实现公网访问本地Office文件

文章目录 本教程解决的问题是&#xff1a;1. 本地环境配置2. 制作本地分享链接3. 制作公网访问链接4. 公网ip地址访问您的分享相册5. 制作固定公网访问链接 本教程解决的问题是&#xff1a; 1.Word&#xff0c;PPT&#xff0c;Excel等重要文件存在本地环境&#xff0c;如何在编…

智能硬件 | AI手机是营销噱头吗?对哪些行业利好?

趁着AI浪潮&#xff0c;手机厂商争先抢滩AI手机领域&#xff0c;智能手机开始步入AI新时代。AI手机不需要借助第三方App&#xff0c;而是通过手机自身的算力&#xff0c;直接成为用户的智能助手。但手机的AI应用能有多少&#xff0c;是否只是手机厂商为促进销量的营销噱头&…

【框架设计】MVC和MVVM对比图

1. MVC&#xff08;Model-View-Controller&#xff09; 单向通信View和Model通过Controller承上启下 2. MVVM&#xff08;Model-View-ViewModel&#xff09; 数据绑定M -> VM -> V DOM事件监听 V -> VM -> M 1. MVC是单向的&#xff0c;MVVM是双向的&#xff0c;…