文章目录
- 项目介绍
- 1、设计任务和事件的规则计算及存储方式
- 2、分布式事务解决方案
- 3、数据库与redis缓存的数据一致性
- 4、缓存穿透
- 5、分布式锁
- 6、消息的延迟推送
项目介绍
此项目提供了小组成员共享任务提醒的功能。
具体功能为:
-
管理员对任务或事件的创建,以及对任务的分配。
-
定时给任务执行人发送提醒。
-
任务执行人完成任务。
额外说明:
- 小组中最少有一个管理员,最多有两个管理员。
- 成员数量不做限制,但根据场景不建议超过10个。
- 除管理员外普通成员没有创建和修改任务的权限。
- 任务:固定时间点给执行人提醒要做的事情。
- 事件:所有成员都可以看到的有时间段的事件,事件本身不具备提醒功能,可以添加关联的任务给相应的执行人发送提醒,比如在事件开始的时候关联一个事件开始的任务,在事件结束的时候关联事件结束的任务,事件创建时可以选择是否同步创建关联任务,任务创建时可以选择是否关联已存在的事件。
1、设计任务和事件的规则计算及存储方式
设计方案
-
首先pass掉子任务全量落表的方式(无限重复规则的按时间落表往后半年或1年的数据),此方式对数据库的压力太大。
-
采用存储任务规则的方式,对已经发生的任务落历史表,未发生的任务使用规则计算发生时间,未发生已修改的存特殊(special)表。
问题
-
开发时间成本问题: 任务规则引擎设计,人力成本及时间成本太大。
-
计算性能问题: 考虑到规则解析计算的性能可能会出现瓶颈,提出此问题。
解决方案:
-
使用rfc-2445标准规范,选用开源项目组件。寻找可靠的开源组件并测试其性能表现,最终经过Jmeter压力测试发现其性能瓶颈出现在http的并发请求上,规则计算不是其性能瓶颈,为减少网络开销,最终对接口进行封装,采用一次请求计算多条规则的方式设计接口。
-
对入口服务的接口采用redis缓存进一步缓解服务器压力,对常用接口的结果进行缓存,如首页的接口,由于其参数一天才变动一次,所以完全可采用缓存的方式存储数据。
2、分布式事务解决方案
问题
微服务架构的系统中总会出现分布式事务的问题,例如:任务服务和事件服务为两个单独的服务,而任务与事件又有关联的操作,修改一条事件需要修改所有与此事件关联的任务,这就涉及到数据的一致性了,需要使用分布式事务来解决问题。
解决方式
解决分布式事务有很多方法,例如:基于可靠消息的最终一致性方案、TCC事务补偿型方案、最大努力通知型。
经过讨论,最终选用了最大努力通知型。详细设计方案为:调用对方服务时,判断是否成功,成功则流程继续,失败就把调用信息存入重试表中,然后继续下面的流程。最后使用定时扫描的方式重新发起接口的调用,重试调用一定次数,如果成功则结束,如果一直未成功则通知管理员处理此信息。
3、数据库与redis缓存的数据一致性
问题
在使用了缓存的业务场景中,例如查询用户的可访问资源,在修改数据库中的这些数据时,可能导致用户查出的缓存数据还是旧数据。
解决方式
设置redis缓存数据过期时间,修改数据时清除对应的缓存,使用户下次查询直接使用数据库的数据。
4、缓存穿透
问题
恶意用户发起攻击,查询一个缓存和数据库中都不存在的数据,造成一直查询数据库导致数据库压力飙升甚至垮掉。
解决方案
1) 接口增加用户权限校验,id做校验,例如 id<0 的直接拦截。
2) 在数据库中取不到的数据存一个空值到Redis,设置缓存有效时间较短,例如30秒后过期。
3) 使用布隆过滤器,布隆过滤器可以做到的效果:一个一定不存在的数据会被拦截掉,可能存在的数据才会去查数据库。因为这个特性,只要设置合理的参数就可以极大的缓解缓存穿透问题。
了解更多:缓存穿透、缓存击穿、缓存雪崩区别和解决方案
5、分布式锁
问题
本系统功能允许一个家庭组中存在多名管理员,所以存在多名管理员同时操作一条数据的可能性,为保证数据的一致性,需要对操作进行加锁处理,微服务中每个服务可能有多个实例,所以存在多个进程的线程操作同一条数据的情况,于是普通单机系统的应用使用的java内存锁不再可靠。
解决方案
分布式锁提供了分布式系统中跨JVM的互斥机制的实现,常用的分布式锁的实现方式有三种:
1、基于数据库实现分布式锁
2、基于缓存(Reids等)实现分布式锁
3、基于Zookeeper实现分布式锁
其中基于数据库的分布式锁实现方式最为简单,但是太过依赖数据库。Redis和Zookeeper中,因为Redis已在本系统中有集成,而且实现起来简单,只要注意Redis实现分布式锁的方式中的几个问题就能做出来一个相对完善的方案。PS:SpringBoot中使用Redis实现分布式锁
6、消息的延迟推送
问题
系统中一条消息推送给客户之后,如果客户没有及时处理需要在间隔一段时间后再发送一次。
解决方案
使用RabbitMQ的死信队列实现消息的延迟发送,其实现方式是设置第一个队列的消息存活时间(TTL),此队列不设置消费者,配置交换机,将过期的消息交换(死信交换DLX)到另一个队列,消费者消费此队列,此时距离消息产生就已经过了一段时间,实现了消息的延迟推送。PS:RabbitMQ的死信队列实现消息的延时消费