实战——缓存的使用
文章目录
- 前言
- 概述
- 实践
- 一、缓存数据一致
- 1.更新缓存类
- 2.删除缓存类
- 二、项目实践(商城项目)
- 缓存预热
- 双缓存机制
前言
对于我们日常开发的应用系统。由于MySQL等关系型数据库读写的并发量是有一定的上线的,当请求量过大时候那数据库的压力一定会上来。
所以采用 MySQL+Redis 这对经典组合来解决高并发问题的。Redis 作为 MySQL 的前置缓存,可以应对绝大部分查询请求,从而在很大程度上缓解 MySQL 并发请求的压力,但是不能一说到缓存脑海中就只有 Redis,这无论在工作还是面试中都不合适,所以我们先全面了解缓存。
注意:缓存不止有redis,需要全面的了解缓存
概述
- 缓存大体可以分为三类(意思就是在整个系统中我们每一层都是有缓存的):
- 客户端缓存;
- 服务端缓存;
- 网络中的缓存。
- 根据规模和部署方式缓存也可以分为:
- 单体缓存;
- 缓存集群;
- 分布式缓存。
我们通过对每一层进行缓存来提高系统的稳定性和效率。
实践
一、缓存数据一致
不论是本地缓存还是redis,我们的基本思路就是:当缓存没有命中的时候,我们就去数据库查询,然后直接放到缓存中,供下次查询以加快查询效率。
但是我们在修改数据的时候,就可能造成数据库和缓存数据一致性的问题。
有好几种解决方案,(两大类:一:更新;二:删除)1. 先更新缓存,再更新数据库2. 先更新数据库,再更新缓存3、先删除缓存,后更新数据库4、先更新数据库,后删除缓存
1.更新缓存类
不论先更新数据库还是先更新缓存,这两种方案都不可取。原因就是不论我们先更新谁后更新谁,就会导致我们前面更新成功后,后面更新的那个万一挂了,我们就难以判断是否成功。所以这类更新的方案,对我们来说都是不可取的。
2.删除缓存类
- 2.1 先删缓存,后更新数据库
-
问题:
- 线程A删除缓存后,更新DB,但是DB的事务并没有提交,线程B进来访问。那么就会重新更新缓存(造成缓存和DB数据不一致)
- 线程A删除缓存后,更新DB,但是DB的事务并没有提交,线程B进来访问。那么就会重新更新缓存(造成缓存和DB数据不一致)
-
解决方案
- 使用延时双删
- 1.先删除缓存,再去更新DB
- 2.当DB更新成功之后,延时个1s,再删除一次缓存
- 使用延时双删
-
-
2.2 先更新数据库,后删除缓存(开发中常用的策略)
-
问题:
- 在缓存失效的且并发的时候会发生(虽然概率比较小但是还是会发生,因为概率小所以我们在开发中常用)
- 在缓存失效的且并发的时候会发生(虽然概率比较小但是还是会发生,因为概率小所以我们在开发中常用)
-
解决方案:
- 1.设置缓存失效时间。
- 2.异步延时删除机制(问题在于当我们删除缓存的时候万一失败需要补偿机制来保证缓存一定删除)
异步补偿删除方案一:
异步补偿删除方案二:
由于方案一中的业务代码的耦合性较高。使用订阅数据库的binlog
-
说到底就是通过数据库的 binlog 来异步淘汰 key,利用工具(canal)将 binlog
日志采集发送到 MQ 中,然后通过 ACK 机制确认处理删除缓存。
先更新 DB,后删除缓存,这种方式,被称为 Cache Aside Pattern,属于缓存更新的设计模式之一。(这是一种最为标准的方案)
二、项目实践(商城项目)
我这边以京东商城为例
像这样的商城首页一定是并发最高的地方,如果我们每次都去数据库查询,很显然是不满足我们高并发的要求的。
像这种任何用户看到的都是一样的结果的数据,在缓存中的命中率是比较高得,所以我们可以考虑引入缓存的方式。
并且我们这里完全可以分为多个key,如促销,轮播图,标签等。
- 首页缓存使用
// 大佬的代码例子public HomeContentResult getFromRemote(){List<PmsBrand> recommendBrandList = null;List<SmsHomeAdvertise> smsHomeAdvertises = null;List<PmsProduct> newProducts = null;List<PmsProduct> recommendProducts = null;HomeContentResult result;/*从redis获取*/if(promotionRedisKey.isAllowRemoteCache()){recommendBrandList = redisOpsUtil.getListAll(promotionRedisKey.getBrandKey(), PmsBrand.class);smsHomeAdvertises = redisOpsUtil.getListAll(promotionRedisKey.getHomeAdvertiseKey(), SmsHomeAdvertise.class);newProducts = redisOpsUtil.getListAll(promotionRedisKey.getNewProductKey(), PmsProduct.class);recommendProducts = redisOpsUtil.getListAll(promotionRedisKey.getRecProductKey(), PmsProduct.class);}/*redis没有则从微服务中获取*/if(CollectionUtil.isEmpty(recommendBrandList)||CollectionUtil.isEmpty(smsHomeAdvertises)||CollectionUtil.isEmpty(newProducts)||CollectionUtil.isEmpty(recommendProducts)) {result = promotionFeignApi.content(0).getData();}else{result = new HomeContentResult();result.setBrandList(recommendBrandList);result.setAdvertiseList(smsHomeAdvertises);result.setHotProductList(recommendProducts);result.setNewProductList(newProducts);}return result;}
redis 初始化key代码初始化注解@PostConstruct
@Service
@Slf4j
public class PromotionRedisKey {// 配置@Value ("${namespace.promotion:prmtd}")private String promotionNamespace;@Value ("${promotion.brand:br}")private String brand;@Value ("${promotion.newProduct:np}")private String newProduct;@Value ("${promotion.recProduct:rp}")private String recProduct;@Value ("${promotion.homeAdvertise:hd}")private String homeAdvertise;@Value ("${promotion.seckill:sk}")private String secKill;// 需要初始化的keyprivate String brandKey;private String newProductKey;private String recProductKey;private String homeAdvertiseKey;private String secKillKey;// 全局缓存控制开关@Value("${promotion.demo.allowLocalCache:true}")private boolean allowLocalCache;@Value("${promotion.demo.allowRemoteCache:true}")private boolean allowRemoteCache;/*** 该注解的方法在整个Bean初始化中的执行顺序:** Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的初始化方法)** 该注解的功能:当依赖注入完成后用于执行初始化的方法,并且只会被执行一次*/@PostConstructpublic void initKey(){brandKey = promotionNamespace + "." + brand;newProductKey = promotionNamespace + "." + newProduct;recProductKey = promotionNamespace + "." + recProduct;homeAdvertiseKey = promotionNamespace + "." + homeAdvertise;secKillKey = promotionNamespace + "." + secKill;StringBuilder logKeyStr = new StringBuilder();logKeyStr.append("[品牌推荐redis主键=").append(brandKey).append("] [新品推荐redis主键=").append(newProductKey).append("] [人气推荐redis主键=").append(recProductKey).append("] [轮播广告redis主键=").append(homeAdvertiseKey).append("] [秒杀redis主键=").append(secKillKey).append("]");log.info("促销系统Redis主键配置:{}",logKeyStr);}public String getBrandKey() {return brandKey;}public String getNewProductKey() {return newProductKey;}public String getRecProductKey() {return recProductKey;}public String getHomeAdvertiseKey() {return homeAdvertiseKey;}public String getSecKillKey() {return secKillKey;}public boolean isAllowLocalCache() {return allowLocalCache;}public boolean isAllowRemoteCache() {return allowRemoteCache;}
}
通过以上首页对缓存的使用,提出一些思考如下:
缓存一定是离用户越近越好,依据这个原则,首页还有优
化的空间,从上面的访问路径可以看到,首页服务需要到 Redis 集群中获得数据用以展示,能不能将缓存的数据再提前呢?于是我们在首页服务内引入了应用级缓存 Caffeine。
什么叫近???
TIPS:Caffeine 基于 Google 的 Guava Cache,提供一个性能卓越的本地缓存(local cache) 实现, 也是 SpringBoot 内置的本地缓存实现,有资料表明 Caffeine性能是 Guava Cache 的 6 倍
Caffeine使用
@Configuration
public class CaffeineCacheConfig {@Bean(name = "promotion")// 缓存预热(指定bean名称)public Cache<String, HomeContentResult> promotionCache() {int rnd = ThreadLocalRandom.current().nextInt(10);return Caffeine.newBuilder()// 设置最后一次写入经过固定时间过期.expireAfterWrite(30 + rnd, TimeUnit.MINUTES)// 初始的缓存空间大小.initialCapacity(20)// 缓存的最大条数.maximumSize(100).build();}/*以双缓存的形式提升首页的访问性能,这个备份缓存其实设置为永不过期更好,* 可以作为首页的降级和兜底方案* 为了说明缓存击穿和分布式锁这里设置了一个过期时间*/@Bean(name = "promotionBak")public Cache<String, HomeContentResult> promotionCacheBak() {int rnd = ThreadLocalRandom.current().nextInt(10);return Caffeine.newBuilder()// 设置最后一次访问经过固定时间过期.expireAfterAccess(41 + rnd, TimeUnit.MINUTES)// 初始的缓存空间大小.initialCapacity(20)// 缓存的最大条数.maximumSize(100).build();}/*秒杀信息在首页的缓存*/@Bean(name = "secKill")public Cache<String, List<FlashPromotionProduct>> secKillCache() {int rnd = ThreadLocalRandom.current().nextInt(400);return Caffeine.newBuilder()// 设置最后一次写入经过固定时间过期.expireAfterWrite(500 + rnd, TimeUnit.MILLISECONDS)// 初始的缓存空间大小.initialCapacity(20)// 缓存的最大条数.maximumSize(100).build();}/*秒杀信息在首页的缓存备份,提升首页的访问性能*/@Bean(name = "secKillBak")public Cache<String, List<FlashPromotionProduct>> secKillCacheBak() {int rnd = ThreadLocalRandom.current().nextInt(400);return Caffeine.newBuilder()// 设置最后一次写入经过固定时间过期.expireAfterWrite(100 + rnd, TimeUnit.MILLISECONDS)// 初始的缓存空间大小.initialCapacity(20)// 缓存的最大条数.maximumSize(100).build();}
}
@Autowired@Qualifier("promotion") // 指定注入相关对象和上面的配置一致,就是在容器中拿对应的值private Cache<String, HomeContentResult> promotionCache;@Overridepublic HomeContentResult cmsContent(HomeContentResult content) {//获取推荐专题content.setSubjectList(homeDao.getRecommendSubjectList(0,4));return content;}/*处理首页推荐品牌和商品内容*/@Overridepublic HomeContentResult recommendContent(){/*品牌和产品在本地缓存中统一处理,有则视为同有,无则视为同无*/final String brandKey = promotionRedisKey.getBrandKey();final boolean allowLocalCache = promotionRedisKey.isAllowLocalCache();/*先从本地缓存中获取推荐内容*/HomeContentResult result = allowLocalCache ?promotionCache.getIfPresent(brandKey) : null;if(result == null){result = allowLocalCache ?promotionCacheBak.getIfPresent(brandKey) : null;}/*本地缓存中没有*/if(result == null){log.warn("从本地缓存中获取推荐品牌和商品失败,可能出错或禁用了本地缓存[allowLocalCache = {}]",allowLocalCache);// 去redis中去,没有再去DB中取result = getFromRemote();if(null != result) {promotionCache.put(brandKey,result);promotionCacheBak.put(brandKey,result);}}/* 处理秒杀内容*/final String secKillKey = promotionRedisKey.getSecKillKey();List<FlashPromotionProduct> secKills = secKillCache.getIfPresent(secKillKey);if(CollectionUtils.isEmpty(secKills)){secKills = secKillCacheBak.getIfPresent(secKillKey);}if(CollectionUtils.isEmpty(secKills)){/*极小的概率出现本地两个缓存同时失效的问题,从远程获取时,只从Redis缓存中获取,不从营销微服务中获取,避免秒杀的流量冲垮营销微服务*/secKills = getSecKillFromRemote();if(!CollectionUtils.isEmpty(secKills)) {secKillCache.put(secKillKey,secKills);secKillCacheBak.put(secKillKey,secKills);}else{/*Redis缓存中也没有秒杀活动信息,此处用一个空List代替,* 其实可以用固定的图片或信息,作为降级和兜底方案*/secKills = new ArrayList<FlashPromotionProduct>();}}result.setHomeFlashPromotion(secKills);// fixme CMS本次不予实现,设置空集合result.setSubjectList(new ArrayList<CmsSubject>());return result;}
缓存预热
缓存预热是为了防止,我们的项目刚发版,可能请求过多造成的数据库压力过大的情况。
使用Spring的启动化机制
@Component
@Slf4j
// 缓存预热,使用Spring启动化机制CommandLineRunner
public class preheatCache implements CommandLineRunner {@Autowiredprivate HomeService homeService;@Overridepublic void run(String... args) throws Exception {for (String str : args) {log.info("系统启动命令行参数: {}", str);}// 缓存预热,可以包括本地缓存或者redishomeService.preheatCache();}}
双缓存机制
为了数据的一致性,本地 Caffeine和redis设置了过期时间,Redis 集群中的数据也会在数据变动后被除。当数据同时过期的时候。可能有以下的情况本地缓存去redis去取数据;本地缓存取DB取数据。那么在耗时上就会产生毛刺现象。
为了避免以上情况,我们使用双缓存的结构。
@Bean(name = "promotion")// 缓存预热public Cache<String, HomeContentResult> promotionCache() {int rnd = ThreadLocalRandom.current().nextInt(10);return Caffeine.newBuilder()// 设置最后一次写入经过固定时间过期.expireAfterWrite(30 + rnd, TimeUnit.MINUTES)// 初始的缓存空间大小.initialCapacity(20)// 缓存的最大条数.maximumSize(100).build();}备份缓存要随着主缓存的变动而变动/*以双缓存的形式提升首页的访问性能,这个备份缓存其实设置为永不过期更好,* 可以作为首页的降级和兜底方案* 为了说明缓存击穿和分布式锁这里设置了一个过期时间*/@Bean(name = "promotionBak")public Cache<String, HomeContentResult> promotionCacheBak() {int rnd = ThreadLocalRandom.current().nextInt(10);return Caffeine.newBuilder()// 设置最后一次访问经过固定时间过期.expireAfterAccess(41 + rnd, TimeUnit.MINUTES)// 初始的缓存空间大小.initialCapacity(20)// 缓存的最大条数.maximumSize(100).build();}
相关文章:

实战——缓存的使用
文章目录前言概述实践一、缓存数据一致1.更新缓存类2.删除缓存类二、项目实践(商城项目)缓存预热双缓存机制前言 对于我们日常开发的应用系统。由于MySQL等关系型数据库读写的并发量是有一定的上线的,当请求量过大时候那数据库的压力一定会上…...

2023年中职网络安全竞赛跨站脚本渗透解析-2(超详细)
跨站脚本渗透 任务环境说明:需求环境可私信博主! 服务器场景:Server2126(关闭链接)服务器场景操作系统:未知访问服务器网站目录1,根据页面信息完成条件,将获取到弹框信息作为flag提交;访问服务器网站目录2,根据页面信息完成条件,将获取到弹框信息作为flag提交;访问…...

Scala的简单使用
文章目录Scala的简单使用(一)交互模式1、命令行方式2、文件方式(二)编译模式1、创建源程序2、编译成字节码3、解释执行对象Scala的简单使用 Scala可以在交互模式和编译模式两种方式下运行 (一)交互模式 在…...

Java之前缀和算法
目录 一.前缀和 1.前缀和介绍 2.编程中的前缀和 二.一维数组的动态和 1.题目描述 2.问题分析 3.代码实现 三.除自身以外数组的乘积 1.题目描述 2.问题分析 3.代码实现 四.和为 K 的子数组 1.题目描述 2.问题分析 3.代码实现 五.形成两个异或相等数组的三元组数目…...

基于GIS计算降雨侵蚀力R因子
一、数据来源介绍 (一)行政边界数据 本文所用到的河北唐山行政边界数据来源于中国科学院资源环境科学与数据中心(https://www.resdc.cn/Default.aspx)。 (二)降水量数据 本文所用到的降水量数据来源于国家…...

大数据时代下的企业网络安全
在大数据技术迅猛发展的今天,网络安全问题已经发展成一个广受关注的热门研究方向。有人说,“大数据下,人人裸奔”,隐私保护、数据防护日益成为广大学者、企业研究的焦点。 面对这种安全威胁,企业必须实施一些有效的信…...

【跟我一起读《视觉惯性SLAM理论与源码解析》】第三章第四章 SLAM中常用的数学基础知识相机成像模型
齐次坐标能大大简化在三维空间中点、线、面表达方式和旋转、平移等操作在齐次坐标下,两个点的叉积结果可以表示一条直线l;也可以用两条直线的叉积结果表示它们的齐次坐标交点,关于叉积其实十四讲解释的还是比较清楚的,和李代数李群的关系可以…...

LeetCode 242. 有效的字母异位词
242. 有效的字母异位词 难度:easy\color{Green}{easy}easy 题目描述 给定两个字符串 sss 和 ttt ,编写一个函数来判断 ttt 是否是 sss 的字母异位词。 注意: 若 sss 和 ttt 中每个字符出现的次数都相同,则称 sss 和 ttt 互为字…...

力扣mysql刷题记录
mysql刷题记录 刷题链接https://leetcode.cn/study-plan/sql/?progressjkih0qc mysql冲!mysql刷题记录1699. 两人之间的通话次数1251. 平均售价1571. 仓库经理1445. 苹果和桔子1193. 每月交易 I1633. 各赛事的用户注册率1173. 即时食物配送 I1211. 查询结果的质量…...

Linux基础命令-lsof查看进程打开的文件
Linux基础命令-uptime查看系统负载 Linux基础命令-top实时显示系统状态 Linux基础命令-ps查看进程状态 文件目录 前言 一 命令的介绍 二 语法及参数 2.1 使用help查看命令的语法信息 2.2 常用参数 2.2.lsof命令-i参数的条件 三 命令显示内容的含义 3.1 FD 文件描述符的…...

常用电平标准
现在常用的电平标准有TTL CMOS LVTTL LVCMOS LVDS PCI等,下面简单介绍一下各自的供电电源、电平标准及注意事项数字电路中,由TTL电子元件组成电路使用的电平。电平是个电压范围。标准输出高电平(VOH): 2.4V标准输出低电平(VOL):0.4V通常输出高…...

小程序开发注意点
1.组件样式隔离注意点 2.methods方法 3.自定义组件的properties参数 4.自定义组件的事件监听 5.纯数据字段 6.插槽 单个插槽 启用多插槽 使用多个插槽 7.属性绑定实现父传子功能 例如在这里有一个组件为<one></one>,那么可以在组件当中传入参数 &l…...

自行车出口欧盟CE认证,新版自行车标准ISO 4210:2023与ISO 8098:2023发布
2023年1月,国际标准化组织ISO发布了新版“自行车以及儿童自行车的测试标准”,即ISO 4210:2023以及ISO 8098:2023,用于取代了SO 4210:2015以及ISO 8098:2015。新版标准一经发布,立即生效。欧盟标准化委员会C…...

2020蓝桥杯真题回文日期 C语言/C++
题目描述 2020 年春节期间,有一个特殊的日期引起了大家的注意:2020 年 2 月 2 日。因为如果将这个日期按 “yyyymmdd” 的格式写成一个 8 位数是 20200202,恰好是一个回文数。我们称这样的日期是回文日期。 有人表示 20200202 是 “千年一遇…...

postman入门到精通之【接口知识准备】(一)
postman入门到精通之【接口知识准备】(一) 目录:导读 前言 接口测试概念 接口测试 接口测试的原理 常用接口测试工具 接口测试基础知识 接口的定义 接口的分类 HTTP接口 Web Service接口 RESTful接口 HTTP请求 统一资源定位符&…...

【算法数据结构体系篇class07】:加强堆
一、手动改写堆(非常重要)!系统提供的堆无法做到的事情:1)已经入堆的元素,如果参与排序的指标方法变化,系统提供的堆无法做到时间复杂度O(logN)调整!都是O(N)的调整!2&am…...

Taro3.x 容易踩坑的点(阻止滚动穿透,弹框蒙层父级定位)
解决弹框滚动的时候,下层也会滚动问题》阻止滚动穿透(react,vue)案例描述:页面展示时需要滚动条才可以显示完整,但是当我们显示弹框的时候,即使不需要滚动条,但是页面仍然可以滚动,并且下层内容会随着滚动变…...

SpringBoot+ActiveMQ-发布订阅模式(消费端)
ActiveMQ消息中间件的发布订阅模式 主题 topictopic生产端案例(配合topic消费端测试):SpringBootActiveMQ Topic 生产端ActiveMQ版本:apache-activemq-5.16.5案例源码:SpringBootActiveMQ-发布订阅DemoSpringBoot集成ActiveMQ Topic消费端的pom.xml<?…...

vscode下使用arduino插件开发ESP32 Heltec WiFi_Kit_32_V3
下载vsCode 添加 arduino 插件 在Arduino IDE 中添加开发板,注意只能用右侧的开发板管理器添加,自己下载之后复制进去的IDE认,但是vsCode不认,搜索ESP32 第一个库里面只有到V2的,没有V3,要安装下面那个 H…...

吐血整理AutoSAR Com-Stack 的配置【基于ETAS】
总目录链接>> AutoSAR入门和实战系列总目录 文章目录01.软件组件和系统说明02.基本软件配置03.系统数据映射04.代码生成05.代码整合06.测试下图显示了基于 AUTOSAR 的 ECU SW 的结构。纵观BSW,大体分为三层。三层模块中,与通信相关的模块称为通信…...

面向对象进阶之元类
6. 元类 Python 中一切皆对象,对象是由类实例化产生的。那么类应该也有个类去产生它,利用 type() 函数我们可以去查看: class A:pass a1 A() print(type(a1)) print(type(A))<class __main__.A> <class type>由上可知…...

【Android AIDL之详细使用】
Android AIDL之详细使用一级目录概述使用场景语法相关编码实践服务端:java文件修改AndroidManifest客户端坑一级目录 概述 AIDL叫Android接口定义语言,是用于辅助开发者完成Android跨进程编程的工具。 从某种意义上说AIDL其实是一个模板,因…...

ASP.NET MVC | 简介
目录 前提 1.教程 2.MVC 编程模式 最后 前提 在学习学过很多课程,但是最主要学的还是ASP.NET MVC这门课程,工作也是用的ASP.NET MVC,所以写一点ASP.NET MVC的东西,大家可以来看看,我自己不会的时候也不用找别的地方…...

95后刚毕业2、3年就年薪50W,才发现,打败我们的不是年龄····
一刷朋友圈,一读公众号,一打开微博,甚至是一和朋友聊天,这些让人焦虑的话题总会铺天盖地的袭来: Ta刚毕业半年,就升职加薪当上了测试主管 (同样是一天24小时,为什么同龄人正在抛弃…...

动态分析和静态分析最主要的区别是什么?
动态分析和静态分析主要的区别是什么? 动态分析和静态分析的主要区别是是否考虑时间因素。 动态分析(dynamic analysis)是相对于静态分析来讲的,动态分析是只改变一下自变量,因变量相应的做出的改变,动态改…...

WebUI 学习笔记
WebUI 学习笔记 背景此插件主要用于在数字孪生方向做 UI 显示的效果。比如一些温度曲线需要显示出来,可以直接用插件,配合html 文件,直接显示出来。 准备工作我们采用4.27 版本进行开发;...

C# 中常见的设计模式附带代码案例
设计模式是一套被广泛应用于软件设计的最佳实践,它们可以帮助开发者解决特定的问题,提高代码的可重用性、可读性和可维护性。本文将介绍 C# 中常见的几种设计模式,并提供相应的示例代码。 工厂模式 工厂模式是一种创建型设计模式,…...

秋招面试问题整理之机器学习篇
文章目录随机森林在决策树的哪些方面做出了改进随机森林里每棵树的权重不一定会变成什么模型方差和偏差,正则化解决的是方差大还是偏差大的问题正则化的方法总结了解VC维吗svd了解吗随机森林在决策树的哪些方面做出了改进 回答思路: 随机森林和决策树有…...

SuperMap超图使用简单笔记
1 需求: 项目使用的是openlayer和Cesium,现在需要使用超图的图层,和引入实景公路功能。 2 使用过程中出现一下疑问点记录如下 : 超图: 北京超图软件股份有限公司是全球第三大、亚洲最大的地理信息系统(G…...

从0探索NLP——神经网络
从0探索NLP——神经网络 1.前言 一提人工智能,最能想到的就是神经网络,但其实神经网络只是深度学习的主要实现方式。 现在主流的NLP相关任务、模型大都是基于深度学习也就是构建神经网络实现的,所以这里讲解一下神经网络以及简单的神经网络…...