黑马苍穹外卖技术亮点 详情
技术亮点总结
1.使用Redis采用一主两从+哨兵的集群方案
【涉及高并发场景下读数据的问题】
使用Redis
当用户数量较多时,用户频繁的访问数据库,数据库压力增大,系统的性能下降。因此使用Redis对数据进行缓存,减小数据库的压力,提高系统的性能和访问速度。
为什么采用一主两从+哨兵的集群方案(解决高并发高可用问题)
进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。
我们项目的redis采用一主二从的集群方案,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中,实现了读写分离,解决了高并发问题。
哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送ping命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
哨兵可以实现自动故障修复:哨兵可以监测到主节点宕机,自动将从节点切换为主节点并发布通知其他从服务器修改配置文件,当旧主服务器恢复后会成为一个新的从节点。
怎样解决高并发高可用问题
主从集群保证高并发,哨兵保证高可用。
怎样解决缓存与数据库的数据一致性问题
数据一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致。
使用旁路缓存模式在缓存与数据库双写时保持一致性。
先更新数据库,后删除缓存。理论上会出现不一致的情况,但概率较小因为缓存的写入速度是比数据库的写入速度快很多。
**更新缓存替换为删除缓存**,那么**下一次执行的查询操作都会从数据库中加载数据**,此时数据一致性就有了保证。
比较方法时注意:缓存被清空后,线程只能到数据库中查询,在数据库中查完就会直接更新缓存。
先查缓存,再查数据库。
**重点在与看结束后数据库与缓存是否一致,用多久一致的。**
(1)先删缓存,再更新数据库:
(多线程调度不确定)可能会导致A删缓存,B去查缓存“空”-->然后去查数据库-->拿着数据库中数据更新缓存-->A更新数据库
结束后:数据库中是A更新后的信息,缓存中是原始信息,这样导致数据库与缓存数据不一致。
(2)先更数据库,再删缓存:
A更数据库,B查缓存-->B拿到原始数据-->A删除缓存
结束后:数据库中新数据,缓存被删即还会获得新数据。【从B在数据库拿到原始数据到写到缓存中,但持续时间相比其他方法短】
(3)延迟双删:
A删缓存,B去查缓存“空”-->然后去查数据库-->拿着数据库中数据更新缓存-->A更新数据库-->延迟删除缓存
结束后:相比1最后缓存与数据库时一致的,但在延迟的那段时间里数据是不一致的
总结:采取旁路缓存使缓存和数据库保持数据的一致性,
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,再删除缓存中对应着数据库更新的数据。
Cache Aside(旁路缓存)策略以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策略。
读策略 :从缓存中读取数据;如果缓存命中,则直接返回数据;如果缓存不命中,则从数据库中查询数据;查询到数据后,将数据写入到缓存中,并且返回给用户。
写策略 :先更新数据库中的记录,再删除缓存记录。
2.使用工厂模式和策略模式实现布隆过滤器解决缓存穿透问题
Bitmap
Bitmap是一种数据结构,它使用位图来表示数据。在处理大量数据时,Bitmap可以通过将每个数据元素映射到一个位,然后使用位运算来对数据进行操作。
通过使用Bitmap,我们可以快速地完成排序、查找和去重等操作。优点极大的节省储存空间。但是,Bitmap也存在一些局限性,例如它只能表示有限的数值类型,并且对于数据稀疏的情况可能不太适用。
布隆过滤器
布隆过滤器是一种用于快速检索一个元素是否可能存在于一个集合中的数据结构。
它的基本原理是利用多个哈希函数,将一个元素映射成多个位,然后将这些位设置为1。当查询一个元素时,如果这些位都被设置为1,则认为元素可能存在于集合中。布隆过滤器的优点在于它的内存占用极少,并且不局限于数值类型。但是,由于哈希冲突的存在,布隆过滤器也存在误判的可能。
缓存穿透
访问的资源不存在,而导致这个不存在的数据每次请求都要到数据库中去查询,导致数据库挂掉
用布隆过滤器解决缓存穿透问题
不直接访问数据库,而是先通过布隆过滤器判断访问的资源是否存在,存在则放行,不存在则拦截。
使用BitMap作为布隆过滤器,通过多个哈希函数来将被访问的元素映射到位数组中的多个位置上,将对应的位置设置为1。
当需要判断一个元素是否在布隆过滤器中时:只需将该元素进行多次哈希,并检查对应的位数组位置是否都为1,如果其中有任意一位为0,则说明该元素不在集合中;如果所有位都为1,则说明该元素可能在集合中(因为有可能存在哈希冲突),需要进一步检查。
使用工厂模式和策略模式解决缓存穿透问题
- 定义布隆过滤器接口:首先定义一个布隆过滤器接口,包括添加元素和判断元素是否存在两个基本操作。
- 实现具体的布隆过滤器类:创建类,实现布隆过滤器接口中的方法。在这个类中,需要定义布隆过滗器的数据结构(比如位数组)、大小等属性。
- 定义哈希策略接口:定义一个哈希策略接口,包含计算哈希值的方法。
- 实现具体的哈希策略类:创建多个具体的哈希策略类,每个类对应一种哈希函数的计算方法。
- 创建布隆过滤器工厂类:定义一个布隆过滤器工厂类,其中包含一个用于创建布隆过滤器对象的工厂方法。工厂方法接受布隆过滤器的大小和哈希策略对象作为参数,并返回一个具体的布隆过滤器对象。
- 使用布隆过滤器工厂:在需要创建布隆过滤器对象的地方,调用布隆过滤器工厂的工厂方法来创建布隆过滤器对象,并传入相应的哈希策略对象。
总结:使用策略模式是创建多个具体的哈希策略类,每个类对应一种哈希函数。并且创建布隆过滤器的工厂类。
当需要创建布隆过滤器对象时,直接调用工厂类并传入相应的哈希策略对象。
这样可以将对象的创建和哈希函数的选择解耦,更灵活的创建对象和选择哈希函数。
好处
使用工厂模式和策略模式来实现布隆过滤器带来以下好处:
(1)解耦性:工厂模式和策略模式的结合可以将对象的创建和哈希函数的选择分离,使得各部分之间的耦合度降低。这样在需要修改布隆过滤器的具体实现或者切换哈希函数时,只需要修改相应的工厂类或策略类,而不影响其他部分。
(2)可扩展性:通过工厂模式和策略模式,我们可以方便地添加新的布隆过滤器实现类和哈希函数策略类,而不需要修改现有代码。这样在需要增加新的布隆过滤器类型或者新的哈希函数时,只需添加相应的类即可。
3.使用Spring Task实现订单超时取消,使用WebSocket实现用户催单。
WebSocket
基于TCP的一种新的网络协议。实现了浏览器与服务器全双工通信——浏览器和服务器只需完成一次握手,两者就可以建立连接,双向数据传输。
WebSocket:两边都可以主动通信。
应用场景:网页聊天、视频弹幕,股票信息实时更新(服务器主动推送到网页上的)
订单超时取消流程
客户下单后未支付,订单一直处于“待支付”状态。
使用Spring Task工具的cron表达式设置定时任务触发时间,每分钟检查一次是否存在支付超时的订单,如果存在超时则将订单状态修改为“已取消”。
用户催单流程
用户在小程序中点击催单按钮后,根据用户id查询是否有支付成功的订单,有则调用WebSocke实现服务端向客户端推送消息,展示用户订单和支付成功信息。
4.使用自定义拦截器和JWT令牌实现用户登录认证,用ThreadLocal存储用户ID
用户登录认证流程
(1)用户登录时,发送用户名和密码给服务器,服务器验证通过则生成一个包含用户信息的JWT令牌,并将其发送给客户端。
(2)客户端受到JWT令牌后,保存在本地中,
(3)每当用户发起请求时,将JWT令牌添加到请求头中
(4)服务器端的自定义拦截器会拦截所有请求,从请求头中提取JWT令牌。
(5)拦截器解析JWT令牌,获取用户信息,并将其存储在ThreadLocal中,本次访问中需要用户信息可以直接从ThreadLocal中拿取,不用每次都去解析JWT令牌。
(6)拦截器可以检查用户是否已经通过认证,通过则处理请求,未通过则返回错误信息。
(7)请求完成后,拦截器remove清楚ThreadLocal里的用户信息
为什么用JWT不用Session
传统的Session用户认证方案:
(1)用户向服务器发送用户名和密码。
(2)服务器验证通过后,在当前**对话(session)**里面保存相关数据,比如用户角色、登录时间等等。
(3)服务器向用户返回一个 session_id,写入用户的 Cookie。
(4)用户随后的每一次请求,都会通过 Cookie将 session_id 传回服务器。
(5)服务器收到 session_id,找到对应的session并获取前期保存的数据,由此得知用户的身份。
这种传统的通过session的方式适用于前后端不分离的情况,因为session是保存在服务器端,因此对于跨域或服务器集群的情况很不友好。
而JWT解决了跨域问题,而且(1)jwt基于json,数据处理方便。(2)使用非对称加密和签名技术,安全性高。(3)资源服务使用JWT,可不依赖认证服务即可完成授权。
为什么用ThreadLocal
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
这样可以保证线程之间的数据隔离,避免了线程安全问题;而且在本次访问中任何需要用户信息时都可以直接在ThreadLocal中拿到信息。
ThreadLocal使用流程
每次执行请求时拦截器将用户信息用set保存在ThreadLocal中,ThreadLocal每次调用set时是一个独立的线程,当另一个用户调用ThreadLocal的set时方法 时,就会新建另一个线程,线程之间互不影响,当对应线程在调用get时候,就会请求到set时候的信息。在拦截器执行过后的方法中添加ThreadLocal线程remove()方法,这样每次请求结束后会将ThreadLocal线程中的数据删除,这样可以防止线程过多内存泄露。
为什么用拦截器不用过滤器、切面
粒度更细: 拦截器可以针对特定的控制器或控制器方法进行拦截,实现精确的拦截逻辑,
而过滤器是基于URL路径进行拦截,无法做到针对具体的控制器或方法,切面也是基于切点进行拦截,粒度相对较粗。
5.使用AOP自定义切面统一记录登录者操作日志
使用AOP切面,将登陆者增删改用户信息的操作信息记录到数据库中。
流程
(1)建表日志信息表
(2)自定义注解,在增删改等方法运行时调用切面
(3)定义切面类,操作者记录:先通过当前请求的请求头拿到JWT令牌并解析出操作者的用户ID,然后记录操作命令和操作时间等信息插入到日志信息表中。
6.使用Redis分布式锁实现高并发商品秒杀,解决超卖问题
【涉及高并发场景下,写数据(更改数据)的问题】
超卖
在秒杀系统设计中,超卖是一个经典、常见的问题,任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难点。
为什么不用单体锁
单体应用难以满足实际高并发访问需求,会将单体应用部署到多个tomcat实例上,由负载均衡将请求分发到不同实例上。
单体锁(synchronized、ReentrantLock)是JVM层面的锁,只能控制单个实例上的并发访问安全,多实例下依然存在数据一致性问题。
分布式锁
分布式锁指的是,所有服务中的所有线程都去获取同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,直到持有锁的线程释放锁。
RedLock解决Redis集群主从不同步数据丢失问题
Redisson使用主从集群模式,主节点挂掉,从节点没有同步到锁的情况:
使用RedLock,针对Redis中所有节点来进行同步,能够保证超过半数的Redis加锁了才算加锁成功,从而保证并发安全。
解决流程
多用户并发操作的情况下,多个用户尝试购买同一件商品,导致商品库存不足或者超卖。
采用Redis提供的Redisson组件实现Redis分布式锁,来控制并发访问。
当一个线程去获取锁,锁的VALUE中存入UUID来保证锁和当前线程绑定,
当前线程获取锁成功开始处理业务时,内部会有watch dog看门狗,每隔10s看当前线程是否还持有锁,如果持有则给锁延长生存时间。
当Redis集群部署时,为了解决主节点挂掉从节点没有同步到锁的情况,使用RedLock,针对Redis中所有节点来进行同步,能够保证超过半数的Redis加锁了才算加锁成功,从而保证并发安全。
另外:用户限流,防止同一用户多次秒杀
使用布隆过滤器记录用户和商品ID来解决。
当用户参与秒杀时,判断是否ID是否记录存在布隆过滤器中,不存在证明该用户是第一次参与秒杀改商品,放行继续后续业务;
过滤器中存在,则禁止继续秒杀。
7.基于Redis使用防重Token和lua脚本,防止重复提交订单
lua脚本作用
使用lua脚本实现原子性的查询并删除操作。为了确保即使多个请求同时到达,也只有一个请求能够成功删除Token。
流程
用户发起请求时,服务端生成唯一的token作为信息凭证。
将Token存入Redis并设定过期时间,防止Redis内存溢出。
在后续请求中要携带这个Token,服务器收到后,在Redis中通过lua脚本进行原子性的查询并删除操作,
如果Token存在并被成功删除说明是第一次请求则继续处理业务生成订单;如果不存在说明是重复请求,则返回错误提示。
———————基本问题————————
1.如何保证操作多张表的时候数据一致性。
在service层的方法上加上@transactional注解
,当发生异常的时候,事务会回滚。
2.什么情况可能导致事务异常
-
数据库连接问题: 如果在事务执行期间数据库连接发生问题,比如连接中断或数据库不可用,事务可能会失败。
-
违反数据库约束: 当尝试向数据库中插入、更新或删除数据时,如果违反了数据库的约束条件,如唯一性约束、外键约束等,事务将失败。
-
死锁: 当多个事务相互等待对方释放锁资源时,可能发生死锁。如果系统无法解决死锁,某些事务将被终止,导致异常。
-
超时: 事务执行时间超过了系统设置的最大时间限制,可能会导致事务异常。这通常是为了避免长时间运行的事务阻塞系统。
-
程序错误: 编程错误可能导致事务异常。例如,在事务中执行的代码中可能包含了错误,或者事务在执行期间遇到了意外情况。
-
并发问题: 在多用户并发访问的环境中,如果不正确地处理并发控制,可能会导致事务异常,如丢失更新、脏读等问题。
-
资源耗尽: 如果事务在执行期间请求的资源(如内存、文件句柄等)超出了系统的可用资源,可能会导致事务异常。
-
中断操作: 在事务执行期间,如果操作系统或硬件发生故障,或者系统被强制关闭,事务可能无法正常完成。
3.项目里哪里用来反射
AOP自定义切面,通过切面和反射完成公共字段填充,统一记录登录者的操作日志,以确保系统安全合规。
Spring 里哪里用了反射
- 依赖注入:Spring Boot 通过反射机制将bean注入到相应的属性或构造函数中。当我们在Spring Boot中使用@Autowired注解时,Spring容器会利用反射机制找到相应的bean并注入到对应的属性或构造函数中。
- Bean的创建:Spring Boot通过反射机制创建bean实例。当我们使用@Component、@Service、@Controller等注解标注一个类时,Spring Boot会利用反射机制创建这个类的实例,并将其管理起来。
- AOP:在Spring Boot的AOP(面向切面编程)中,反射机制被用来动态代理目标对象。通过反射,我们可以在运行时动态创建一个代理对象,并将目标对象的方法调用转发到代理对象上,从而实现切面编程。例如事务管理、日志记录等。
4.支付功能实现?
git的使用
解决git的冲突问题
获取最新代码git pull origin master;
查看哪些文件冲突git status;
打开冲突文件;
手动修改冲突文件:编辑文件,根据需要选择保留或合并修改。删除冲突标记和不需要的代码,留下你需要的变更。add 到本地仓库。git add path/to/conflicted_file;
merge合并文件git merge -continue;
commit push
git commit -m "xx"
git push origin master
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)