博客和GitHub
分工
杨明哲负责制定规则和部分牌型判定,我负责API文档,部分牌型,服务器端逻辑实现,服务器端AI实现
思路和实现
架构图
说明
服务器和客户端采用HTTP方式交互。交互数据类型(MIME)采用application/json
。考虑到每个人出牌的独立性,因此采用异步比赛,异步结算的方式。每个人通过开局接口拿到牌,出牌,服务器端保存,并使用一个周期性运行的程序对牌进行判定和积分结算。然后,客户端可以使用历史记录接口查询结果。为了认证身份,要求每个玩家绑定教务处帐号。
异步比赛的设计让玩家可以独立开局出牌,不必等待其他玩家;同时,这样结算程序、服务器端AI可以单独抽离出来,做成独立的程序或微服务(虽然最后还是采用了大内核的形式),这样可以降低服务器负担,也降低了系统耦合。
代码组织
代码采用Java和Kotlin混合编写,其中,牌型判定和计算部分主要使用Java实现,其余部分使用Kotlin
Java代码组织
主要有两个包:
compare
: 主要是牌型判定,排序,服务器端AI和常数定义.logic
: 主要是工具类,缓存,牌的数据结构定义
这些代码主要是从一个叫[NeatlyServer]嫖来的,因此代码规范和风格比较奇怪。
Kotlin代码组织
主要分为8个包:
config
: 配置包,主要存储配置类,用于配置服务器程序的全局选项。controller
: 控制器包,实现了最前端的逻辑,主要是接收请求,参数检查,并调用对应的Serviceexception
: 异常包,主要用于自定义异常。model
: 数据模型包,主要放置领域对象和数据传输对象obsolete
: 弃用的代码repository
: 数据仓库,都是接口,由Spring Data JPA实现service
: 服务包,主要的业务逻辑代码util
: 工具类
关键代码
都挺关键的
性能改进
问题
在测试时发现提交时程序和数据库CPU占用都很高
分析
由于数据库CPU占用高,因此推测问题主要和数据库交互有关,进一步的性能剖析发现调用栈在CombatRepository::save()
花费了大量时间,此方法由Spring Data JPA在运行时动态实现,无法查看代码。但查阅资料发现,Spring Data JPA在Many-to-One关系的One一方修改有大量的性能开销,因为它需要去检查Many的其他部分,而我的代码就是像这样
combat.users!!.add(userCombat)
combatRepository.save(combat)
在One一方进行修改,造成了较大开销
解决
修改代码为在Many一方,也就是关系的持有者一方修改
val userCombat = UserCombat(
...
combat = combat
)
userCombatRepoistory.save(userCombat)
问题解决,CPU占用正常
单元测试
由于时间不足没有自动化测试程序,但使用Kotlin REPL对工具函数进行了手动测试,编写了客户端程序对服务端其他逻辑进行测试
GitHub记录
问题
描述
并发程序的数据一致性问题,主要表现为多人加入导致战局人数超限
尝试
首先是加锁,查找可用战局时加悲观锁,保证一次只有一个线程在访问战局。
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select c from CombatDO c where id in (select uc.id.combatId from UserCombatDO uc where not exists (select uc2 from UserCombatDO uc2 where uc2.id.combatId = uc.id.combatId and uc2.id.userId = :playerId) group by uc.id.combatId having count(uc.id.userId) < 4)")
fun findAllAvailableRoomForPlayer(@Param("playerId") playerId: Int): List<CombatDO>
但是这样仍然没有解决问题,原因是数据库是先查找,后加锁,虽然保证了只有一个线程访问,但数据可能已经无效了,而且对大量数据加悲观锁导致性能降低。
同时,我观察到尽管我在开局加入了重试机制,但在发生异常时仍然无法开局。原因是GameController::open()
开启了事务,GameService::joinCombat()
也被注解了@Transactional
,默认继承了GameController::open()
开启的事务,然后GameService::joinCombat()
异常返回时将事务标记为rollbackOnly
,导致事务回滚。
因此我选择去除GameController::open()
的事务注解。不加锁查找战局,然后重试GameService::joinCombat()
,在其中加锁和校验。如无法加入再开新局。
是否解决
是
收获
并发程序设计真nm神奇学习了一堆数据库和并发程序设计的知识
评价队友
值得学习
- 十分有想法
需要改进
- 十分有想法
PSP
我觉得这个辣鸡表格没什么作用,谁tm写代码的时候还会特意去记个时,写一半想起来计时早就不知道什么时候开始的了
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 20 |
Estimate | 估计这个任务需要多少时间 | 20 | 20 |
Development | 开发 | 1065 | 2520 |
Analysis | 需求分析(包括学习新技术) | 75 | 180 |
Design Spec | 生成设计文档 | 60 | 480 |
Design Review | 设计复审 | 60 | 60 |
Coding Standard | 代码规范(为开发制定合适的规范) | 60 | 60 |
Design | 具体设计 | 120 | 300 |
Coding | 具体编码 | 600 | 1200 |
Code Review | 代码复审 | 60 | 120 |
Test | 测试(自我测试,修改,提交修改) | 30 | 120 |
Reporting | 报告 | 60 | 120 |
Test Report | 测试报告 | 20 | 20 |
Size Measurement | 计算工作量 | 10 | 10 |
Postmortem & Process Improvement Plan | 事后总结并提出过程改进计划 | 30 | 90 |
合计 | 1145 | 2660 |