【AIAppMarket项目】第六阶段-市场平台的系统设计优化

本阶段任务

  • 幂等设计
  • 线程池隔离.
  • 统计分析功能
  • 应用分享功能

幂等设计

在本项目的用户答题功能中,虽然调用多次 AI 的问题已经在之前通过缓存解决,但仍然要防止用户不小心点击多次导致产生多条回答记录,因此需要对接口进行幂等处理。

方案选型

数据库唯一索引(实现成本较低)

在之前,我们将数据库用户回答记录表中的回答 id 号字段配置成唯一索引, 用户完成一次完整答题并提交会执行 insert 语句, MySQL根据唯一索引天然阻止相同订单号数据的插入,我们可以 catch 并报错。

try {
	boolean result = userAnswerService. save(userAnswer);
	ThromUtils.throwIf(!result, ErrorCode.OPERATION ERROR);
} catch(DuplicateKeyException e) {
	// 忽视/处理错误
}

当在数据库中插入、更新或执行其他操作时,违反了唯一键约束时,就会抛出 DuplicateKeyException 异常。

数据库乐观锁

使用乐观锁来实现数据库某数据的幂等性是一种常见的解决方案,特别是在高并发环境下。乐观锁通常依赖于版本号时间戳等机制。
例如一个订单场景:

@Entity
public class Order {
    @Id
    private Long id;
    private String status;
    private BigDecimal amount;

    @Version
    private Integer version;

    // Getters and Setters
}
@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public void updateOrder(Long orderId, String newStatus, BigDecimal newAmount) {
        try {
            Order order = orderRepository.findById(orderId)
                    .orElseThrow(() -> new RuntimeException("Order not found"));

            // 更新订单状态和金额
            order.setStatus(newStatus);
            order.setAmount(newAmount);

            // 保存订单,JPA 会自动处理乐观锁
            orderRepository.save(order);
        } catch (OptimisticLockingFailureException e) {
            // 处理乐观锁异常
            System.out.println("Optimistic lock failure: " + e.getMessage());
            // 可以选择重新尝试更新或抛出异常
            throw new RuntimeException("Failed to update order due to concurrent modification");
        }
    }
}

@Version 注解:在实体类中使用 @Version 注解字段,在每次更新时,JPA 会自动检查版本号是否匹配。
乐观锁异常处理:在 updateOrder 方法中捕获 OptimisticLockingFailureException 异常,以处理并发更新冲突。可以选择重新尝试或抛出异常。
事务管理:确保 updateOrder 方法被 @Transactional 注解包裹,以确保事务的一致性和原子性。

分布式锁

导致数据错乱的元凶很多时候都是“并发修改",因此可以通过加锁来限制关键代码的执行。

使用数据库唯一索引的实现

首先,我们将生成答题记录 id 的时间节点从插入数据时提早到前端进入答题页面的时候,然后用户提交回答的时候,前端不仅提交用户的选项,同时也需要带上这个全局唯一id(后端的DTO类要添加字段)。最后,后端将这个 id 保存到数据库的某个唯一索引字段, 利用数据库实现幕等性。
在这里,我们使用了 Hutool 工具类提供的 IdUtil 工具类来基于雪花算法生成唯一 idIdutil.getSnowflakeNextId()

使用雪花算法生成唯一 id 的优点如下:

  1. 分布式高效性
    雪花算法的设计可以在分布式环境中高效运行。每个节点都可以独立地生成唯一ID,而不需要相互协调或通信,这极大地提高了系统的可扩展性和效率。
  2. 唯一性
    雪花算法保证生成的ID是全局唯一的。这是通过时间戳、机器ID和序列号的组合实现的,确保在不同机器和不同时间生成的ID不会重复。
  3. 时间有序性
    雪花算法生成的ID是基于时间戳的,因此它们具有递增的时间顺序。这对于需要按时间排序的场景非常有用,例如日志记录、数据库插入等。
  4. 高性能
    雪花算法的计算开销非常低。生成一个ID通常只需要几个纳秒,这使得它非常适合高并发、高吞吐量的应用场景。
  5. 可配置性
    雪花算法的各个部分(时间戳、机器ID、序列号)的位数可以根据实际需求进行调整,以适应不同的系统规模和需求。例如,可以增加机器ID的位数以支持更多的节点,或增加序列号的位数以支持更高的并发度。
  6. 无需中心协调
    与传统的集中式ID生成器(如数据库自增ID)不同,雪花算法不需要一个中心节点进行协调,这避免了单点故障,提高了系统的可靠性和可用性。
  7. 实现简单
    雪花算法的实现相对简单,不需要复杂的依赖或配置,容易集成到各种系统和语言中。

雪花算法的结构:
符号位(1位):固定为0,表示正数。
时间戳(41位):通常表示自定义纪元开始到当前时间的毫秒数。41位的时间戳可以表示69年的时间。
机器ID(10位):可以标识1024台机器或节点。
序列号(12位):同一毫秒内的并发序列号,最多可以表示4096个并发。

在UserAnswerController下新增生成 id 的接口:

@GetMapping("/generate/id")
public BaseResponse<Long> generateUserAnswerId() {
	return ResultUtils. sccess(IdUtil.getSnowflakeNextId));
}

并在前端中添加对应的 id 传递逻辑即可。

线程池隔离

上一期使用 RxJava 实现Al题目生成的时候 (平台性能优化),用schedulers.io() 方法一个I/O密集型线程池来处理 Al 返回的流。
Java 8 并发流的线程池是全局共享的,这里最大的问题就是相互影响。
假设系统中的一个服务出 bug 导致的线程池里的所有线程都被阻塞了,其他任务就可能被阻塞住。
所以,我们这里也引入了线程池隔离,优点:
1.故障隔离,缩小事故范围。
2.资源隔离,防止业务之间抢占资源。同时支持更精细化地管理资源,比如不重要的场景给小一点的线程池,核心场景配置大线程池。
3.性能优化,-些业务场景的任务是CPU密集型,一些是I/O密集型,不同任务类型需要配置不同的线程池。
查看schedulers.io()的源码:
在这里插入图片描述
在这里插入图片描述
可以发现生成默认线程池确实是全局共享的。

方案设计

综上,我们考虑给 VIP 用户定制一个专用的线程池(配置 VipSchedulerConfig ),改造Al生成题目接口,根据用户类型选择不同的线程池:

@Configuration
@Data
public class VipSchedulerConfig {
    @Bean
    public Scheduler vipScheduler(){
        ThreadFactory threadFactory = new ThreadFactory() {
            private final AtomicInteger threadNumber = new AtomicInteger(1);

            @Override
            public Thread newThread(@NotNull Runnable r) {
                Thread thread = new Thread(r, "VIPThreadPoll-" + threadNumber.getAndIncrement());
                thread.setDaemon(false); // 设置为非守护线程,确保任务顺利执行完成
                return thread;
            }
        };

        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10,threadFactory);
        return Schedulers.from(scheduledExecutorService);
    }
}
	    // 注入对象
	    @Resource
    	private Scheduler vipScheduler;
    	
        // 为尊贵的vip用户定制线程池
        Scheduler scheduler = Schedulers.io();
        if ("vip".equals(loginUser.getUserRole())){
            scheduler = vipScheduler;
        }

统计分析功能

分析哪些App更热门,每种App当中的答案分布情况。

实现方案-关系型数据库

本项目将采用关系型数据库的方式实现,借助 MyBatis 自主编写 SQL 来实现数据的统计分析。

后端

在这里,我们使用 Java 注解的方式实现:
基于Java注解写在xxMapperjava中,在mapper层的接口类方法利用@select@Update@Insert@Delete 等注解,在注解内填写自定义SQL询,即可实现查询、更新、存储、删除。
例如,统计回答数最多的前 10 个应用:

    @Select("select appId, count(userId) as answerCount from user_answer\n" +
            "    group by appId order by answerCount desc limit 10;")
    List<AppAnswerCountDTO> doAppAnswerCount();

每条回答都会对应一个 userId ,因此统计 userId 的数量即可。

前端

使用的是 vue-echarts ,需要先安装:npm i echarts vue-echarts
在前端展示中,
分析哪些App更热门:柱状图
每种App当中的答案分布情况:饼状图
在这里插入图片描述

应用分享功能

为了提高网站和应用的影响力,可以开发便捷应用分享功能。
主要是前端的开发。
主页:
在这里插入图片描述
APP 详情页:
在这里插入图片描述
qrcode :原理是将链接作为文本,转换为二维码图片。

// 分享链接
const shareLink = `${window.location.protocol}//${window.location.host}/app/detail/${props.id}`;

// 分享
const doShare = (e: Event) => {
  if (shareModalRef.value) {
    shareModalRef.value.openModal();
  }
  // 阻止冒泡,防止跳转到详情页
  e.stopPropagation();
};

小结

后续还可以在答题评分分析结果等页面增加分享功能,类似于大家做了测评之后可以转发结果,可以在此基础上增加 App 的传播影响力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值