项目学习总结
项目学习总结
xukun答题平台:
1.策略模式实现多种评分算法
使用不同评分算法进行判题的业务场景天然适配策略模式。不同算法对应的就是不同的策略,所以使用策略模式来封装评分算法,便于维护和扩展。
具体实现方式:
1.定义ScoringStrategy接口,约束了评分的入参和出参
2.定义ScoringStrategyConfig注解,一共有两个属性标识了策略对应的应用属性和评分类型
3.每种评分算法都实现ScroingStrategy接口,且标注对应ScroingStrategyConfig注解
4.定义ScroingStrategyExecutor方法作为驱动类,内部利用Spring注入ScroingStrategy实现类列表,得到所有评分算法。定义execute方法,遍历评分算法列表,通过反射获取策略实现类注解上的属性即可筛选出对应的评分算法。
2.Prompt优化
首先要明确想从AI模型中得到什么样的输出结果,然后选择合适的模型,在写Prompt的时候可以参考官网的配置文档对标点符号优化、让GLM进行角色扮演、提供更具体的细节要求等等,最终能够稳定的返回想要的结果。
3.流式API
ChatGLM的AI SDK提供了流式调用的能力,所以我自主封装了AI Manager简化AI流式调用的传参,返回值是Flowable响应式对象。然后利用RxJava框架来处理AI实时返回的Stream流数据,将AI返回的字符串结果拼接成一道道题目,通过SSE实时推送给前端
4.SSE实时推送
SSE是一种从服务器到客户端的单向实时数据传输技术,基于HTTP协议实现的持久的连接,不需要客户端频繁轮询,实现比较简单,使用的是纯文本格式text进行数据传输
适用场景:实时更新、推送系统通知、AI页面对话
代替方案:轮询(前端主动要数据)
5.RxJava链式调用处理AI异步数据流
RxJava是一个基于事件驱动的、利用可观测序列来实现的异步编程类库。
1.提供了响应式模型使得代码更具有可读性,比如doOnNext、doOnError
2.提供了丰富的操作符,简化异步操作、事件处理、数据转化等。比如map、filter、flatMap
3.简化线程管理,可以很容易在不同线程重进行数据流处理,比如observerOn、subscribeOn
首先流式调用AI接口,利用RxJava的操作符链式调用处理AI实时返回的异步数据流。先通过map获取并处理字符串过滤掉特殊字符比如\n换行符、filter过滤空值、flatMap映射字符串为单个字符再通过括号平衡算法取出单道题目。
核心的应用场景就是UI场景,像Android开发都会用到RxJava。UI场景天然设计到响应和事件这两点,比如我们在手机APP上点击某个按钮,对应app会弹出某个页面,这个按钮就是一个事件,弹出的页面就是对应的响应。
6.括号平衡算法
因为AI生成的题目是JSON结构的,所以题目的左括号数一定等于右括号数。我们可以遍历当前字符串,用一个计数器来统计括号数量,当出现左括号时计数器+1,出现右括号时计数器-1,当计数器为0时代表是一道完整的题目,这时候通过SSE返回给前端。
7.Caffeine本地缓存,在项目中应用和设计
因为是单体项目,所以不考虑分布式或者扩容,选择了成本比较低的本地缓存。主要从以下几个方面来实现:
1.缓存key设计
不同用户可能会对同样的题目做出同样的选择,就会得到一样的结果,所以可以将应用ID和答案结果作为key
2.缓存value设计
缓存AI回答的结果,因为要传到前端,为了可读性还是保留了JSON结构,如果真正上生产的话可以进一步压缩空间,存放二进制或者其他的结构,也可以利用哈希算法(md5)来压缩。
3.设置缓存过期时间
缓存过期时间是必须要设置的,因为产生的回答结果非常多,所以要设置合理的过期时间,这个项目设置的是1天。
4.缓存相关问题处理
需要注意缓存过期后的处理,防止产生缓存击穿、缓存穿透、缓存雪崩等问题。在项目中通过Redisson分布式锁解决了缓存击穿问题。
Redisson:基于Redis的Java调用类库。提供了丰富的分布式对象,包括分布式锁、分布式集合、分布式对象等功能,简化Redis使用的复杂性
5.缓存技术
在项目中一般可以分为两种:本地缓存和分布式缓存
本地缓存可以使用哈希表、Caffeine等实现
分布式缓存可以使用Redis、Memcached等等实现
8.Sharding JDBC分片分表
ShardingJDBC使用简单,只要在配置文件中写好数据源信息、分片算法、分表依赖的列等,就能自动实现分库分表。原理就是改写SQL
本项目中,对数据增长较快的用户答题记录表进行分表,选择应用的ID作为分表键,采用取模分片算法比如将appId%2=0的分到answer_0表,将appId%2=1的分到answer_1表,写完配置之后,Sharding-JDBC会根据配置执行分表的逻辑。
9.Echarts可视化统计图
首先后端定义相关要封装一些要统计字段的实体类,然后编写对应的接口,便于前端调用,前端引入Echarts的模块包,选取数据要展示的图表类型,比如柱状图、饼图…,然后根据官方文档进行配置
算法平台:
1.代理模式和工厂模式-减少重复代码
代理模式是一种结构型设计模式,在不改变原有对象的前提下,通过引入一个代理对象来控制对原有对象的访问
使用代理模式是因为在调用代码沙箱前后进行统一的日志操作,使用代理模式对代码沙箱进行封装,能够在不改变代码沙箱调用实现类前提下,集中增强代码沙箱的能力
工厂模式是一种创建型的设计模式,提供了一种封装对象创建过程的方法。工厂模式将对象的创建和使用分离,让工厂类负责创建对象实例,而不是在代码中使用new操作新建对象。
使用工厂模式是为了简化代码沙箱调用实例的获取,根据代码沙箱的类型字符串来生成对应的代码沙箱调用实现类。
2.黑白名单+字典树
先定义一个黑白名单,比如哪些操作是禁止的,可以就是一个列表:
1 | private static final List<String> blackList = Arrays.asList("Files", "exec"); |
还可以使用字典树代替列表存储单词,用更少的空间存储更多的敏感词汇,并且实现更高效的敏感词查找。
此处使用 HuTool 工具库的字典树工具类: WordTree
缺点:
1)无法遍历所有黑名单
2)不同的编程语言,对应的领域,关键词都不一样,限制人工成本很大
字典树:
字典树是一种树形结构,典型应用是用于统计,排序和保存大量字符串。主要思想是利用字符串的公共前缀来节约存储空间。
在项目中,采用数组的方式创建字典树,然后把一些可能导致安全问题的操作命令存放进去
3.JavaRuntime的exec实现Java的编译与执行
JavaRuntime表示当前虚拟机的运行环境,exec(String command)表示可以用Java代码去执行cmd命令,首先代码经过代码沙箱编译之后得到.class文件,将编译后生成的二进制class文件使用exec方法在虚拟机隔离执行
4.SecurityManager对代码进行权限控制
SecurityManager Java安全管理器是Java提供的安全性工具,用于控制Java应用程序对系统资源的访问和执行权限。通过定义安全策略文件来管理和限制Java程序的操作,确保程序不会访问系统资源和执行危险操作。
本项目通过继承SecurityManager类自定义安全管理器,通过重写checkWrite和checkExec方法限制用户写文件和执行文件。然后修改代码沙箱执行Java代码的命令,绑定定义好的安全管理器(-Djava.security.manager=MySecurityManager),从而限制用户代码中部分危险操作。
5.API签名认证,校验请求头密钥
API签名认证算法是一种用于验证API请求的合法性和完整性的机制,给接口使用API签名认证算法,可以增强API的安全性,防止未经授权的用户访问、防止恶意用户篡改请求数据。
在项目中具体实现步骤如下:
1.生成密钥对:用户生成唯一的密钥对(accessKey和secretKey)
2.加密:对密钥采用加密算法进行加密,比如md5
3.将加密好的密钥加入到鉴权请求头中,当用户发送请求调用API的时候会先校验请求头中的密钥是否一致,否则不会放行请求。
6.重构为微服务项目
本项目重构为微服务架构的主要原因是将判题服务和核心业务(用户和题目服务)进行分离解耦。即使判题服务因为代码沙箱压力过大导致阻塞宕机,也不会影响核心业务的运行,而且使用微服务架构,判题服务可以独立于主服务器进行部署和扩展,灵活性更强。
7.异步处理-防止判题时间过长
由于判题服务需要调用代码沙箱,所以使用RabbitMQ消息队列对判题操作进行异步化解耦。
改造后的业务流程:用户提交题目时,由题目服务发送一条消息到队列,然后判题服务监听到该队列的消息进行判题处理,同时异步更改题目的判题状态。
优点:
1.对用户:不需要在前端同步等待,优化体验。
2.对系统:解耦了题目服务和判题服务,两者不需要相互调用,即使判题服务宕机。题目服务仍然可以发送判题任务到队列,等待判题服务恢复后处理。
8.RabbitMQ-通过Direct交换机转发判题服务消费
RabbitMQ主要有4种交换机:
1.Direct直连交换机:根据消息的路由键将消息发送到匹配的队列上。通常用于点对点通信。
2.Fanout广播交换机:将收到的消息发送到所有与之绑定的队列上,忽略路由键。通常用于广播消息给多个消费者。
3.Topic主题交换机:根据消息的路由键和绑定队列的通配符模式来进行匹配,将消息发送到符合匹配规则的队列上。通常用于支持灵活的消息路由、相对复杂的业务场景。
4.Headers头交换机:根据消息的头部信息来进行匹配,将消息发送到符合匹配规则的队列。适用于消息头部信息进行路由。
在本项目中使用了Direct交换机,因为项目只有1种生产者、1组消费者、1种消息类型,Direct交换机的点对点通信能够满足需求。
9.异步更新提交状态,优化判题操作执行时间,提高吞吐量
用户提交题目后,由题目服务发送一条消息到队列,然后判题服务监听到该队列的消息进行判题处理,同时异步更改题目的判题状态。
当前项目在判题场景下耗时最长,对判题操作和提交状态异步执行,执行判题需要的时间大大缩短,有明显提升。吞吐量(50题/min->500题目/min)大大提升。