• 【代码笔记】高并发场景下问题解决思路


    高并发指的是在单位时间内,瞬时流量激增,系统需要同时处理大量并行的请求或操作。这种情况通常出现在面向大量用户或服务的分布式系统中,尤其是当用户请求高度集中时,比如促销活动、秒杀活动、注册抢课、热点事件、定时任务调度等。

    在高并发发生时,系统可能存在以下问题:
    1.系统性能维度

    • 性能瓶颈:高并发可能导致系统资源(如CPU、内存、磁盘I/O、网络带宽)达到瓶颈,影响整体性能。
    • 响应延迟:系统处理请求的响应时间可能因并发量增加而延长,影响用户体验。
    • 系统过载:超出系统设计容量的并发请求可能导致系统过载,甚至宕机。
    • 容错性差:在高并发下,系统的容错性受到考验,单点故障可能导致整个服务不可用。

    2.用户行为维度

    • 不可预测性:用户行为在高并发期间可能变得难以预测,导致难以准确评估系统负载。
    • 用户操作冲突:大量用户同时进行操作可能导致冲突,如抢票、抢单等场景。
    • 用户体验下降:由于系统响应变慢,用户体验可能显著下降。

    3.数据处理维度

    • 数据不一致:在高并发写入时,缺乏合适的锁机制可能导致数据不一致。
    • 事务管理困难:高并发环境下,保持事务的ACID属性变得更加困难。
    • 数据库压力:高并发可能导致数据库连接数过多,查询和事务处理速度下降。

    二.策略

    为了应对高并发带来的压力,在高并发场景下,系统设计和优化可以从以下几个维度进行调整:

    1. 架构设计维度

    • 服务拆分:将单体应用拆分成多个微服务,实现服务的独立扩展和维护。
    • 负载均衡:使用硬件或软件负载均衡器,如Nginx或HAProxy,分配网络流量和请求。
    • 无状态设计:确保应用服务器无状态,可以水平扩展。无状态设计是构建可伸缩、高可用系统的重要原则,特别是在高并发场景下。在无状态设计中,服务器不会存储任何关于客户端请求的信息,每个请求都是独立的,不依赖于之前的任何请求。

    2. 数据库与存储优化维度

    • 数据库优化:对数据库进行定期的维护,如优化索引、更新统计信息。
    • 缓存应用:使用缓存减少数据库访问,如Redis进行热点数据缓存。
    • 存储优化:使用SSD代替HDD,提高I/O效率;考虑使用分布式存储系统。

    3. 缓存策略维度

    • 多级缓存:实现应用层、服务层和数据库层的多级缓存机制。
    • 缓存淘汰策略:合理配置缓存淘汰策略,如LRU(最近最少使用)。
    • 热点数据优化:对频繁访问的数据进行特殊缓存处理。

    4. 代码与应用优化维度

    • 异步处理:将非实时性的任务异步化,使用消息队列如Kafka或RabbitMQ。
    • 代码审查:定期进行代码审查,优化代码逻辑和结构。
    • 资源池:使用线程池、数据库连接池等资源池技术,提高资源利用效率。

    5. 运维与监控维度

    • 实时监控:部署实时监控系统,如Prometheus或Zabbix,监控系统性能指标。
    • 日志管理:集中管理日志,使用ELK(Elasticsearch, Logstash, Kibana)堆栈进行日志分析。
    • 自动扩缩容:结合云服务提供的自动扩缩容功能,根据流量自动调整资源。

    通过上述维度的策略实施,可以显著提升系统在高并发环境下的性能和稳定性。然而,每个系统的具体场景和需求都有所不同,因此在实施优化时需要根据实际情况进行定制化的调整。

    三.例子

    在大学抢课场景,课程的人数限制为30个学生,系统面临的主要问题包括:

    1. 高并发处理:在抢课开始时,系统可能会收到大量并发请求,需要设计以承受这种瞬时流量。
    2. 数据一致性:确保在高并发下,课程的选课名额不会超卖。
    3. 公平性:确保所有学生在抢课开始时都有机会选到课程。
    4. 系统稳定性:在高负载下,系统需要保持稳定,避免宕机。

    领域模型:

    • Course:代表一门课程,包含课程ID、课程名称、剩余名额等属性。
    • Student:代表学生,包含学生ID、姓名等属性。
    • Enrollment:代表选课记录,包含学生ID、课程ID、选课时间等信息。

    实现逻辑:

    1. 初始化课程:在系统中预先定义好每门课程的信息,包括课程ID、课程名称、容量等。
    2. 发布课程:将课程信息发布到选课系统中,学生可以查看到可选用课的列表。
    3. 学生选课:学生发送选课请求到系统。
    4. 获取分布式锁:系统尝试获取对应课程的分布式锁,确保同时只有一个请求能修改名额。
    5. 检查名额:检查Redis中该课程的剩余名额是否大于0。
    6. 更新名额:如果名额足够,更新Redis中该课程的剩余名额,并记录选课信息到数据库。
    7. 释放锁:完成名额更新后,释放分布式锁。
    8. 返回结果:向学生返回选课成功或失败的结果。

    Demo

    以下是使用Spring Boot和Redis实现大学抢课逻辑的示例代码

    CourseController.java - REST 控制器用于处理课程注册请求:

    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequestMapping("/api/courses") // 定义API的基础路由
    public class CourseController {
    
        @Autowired
        private CourseService courseService; // 注入课程服务类
    
        @PostMapping("/{courseId}/enroll") // 定义POST请求,用于抢课操作
        public ResponseEntity<?> enrollStudent(@PathVariable("courseId") String courseId, // 课程ID作为路径参数
                                               @RequestParam("studentId") String studentId) { // 学生ID作为请求参数
            boolean result = courseService.enroll(courseId, studentId); // 调用服务类的方法进行抢课
            if (result) {
                return ResponseEntity.ok("Enrollment successful!"); // 如果成功,返回成功响应
            } else {
                return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("Course is full."); // 如果失败,返回服务不可用响应
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    CourseService.java - 服务类使用Redis进行分布式锁控制:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.data.redis.core.script.RedisScript;
    import org.springframework.stereotype.Service;
    
    @Service
    public class CourseService {
    
        @Autowired
        private StringRedisTemplate redisTemplate; // 注入Redis字符串模板类
        @Autowired
        private EnrollmentRepository enrollmentRepository; // 注入选课记录的持久层接口
    
        private static final String LOCK_SCRIPT = // 定义Lua脚本用于获取锁
            "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) == 1 then return 1 else return 0 end";
    
        public boolean enroll(String courseId, String studentId) {
            String lockKey = "course:" + courseId + ":lock"; // 定义锁的key
            String studentKey = "course:" + courseId + ":student:" + studentId; // 定义学生的key
    
            // 使用Redis的Lua脚本原子地尝试获取锁,使用随机值和1000ms超时
            Boolean acquiredLock = redisTemplate.execute(new DefaultRedisScript(LOCK_SCRIPT),
                                                            Collections.singletonList(lockKey),
                                                            studentId,
                                                            String.valueOf(1000L));
    
            if (Boolean.TRUE.equals(acquiredLock)) {
                try {
                    // 检查学生是否已经选过这门课程
                    if (redisTemplate.opsForSet().isMember(studentKey, studentId)) {
                        return false;
                    }
    
                    // 检查剩余座位数
                    Integer remainingSeats = redisTemplate.opsForValue().increment("course:" + courseId + ":seats", -1);
                    if (remainingSeats >= 0) {
                        // 选课成功,将学生添加到选课集合中
                        redisTemplate.opsForSet().add(studentKey, studentId);
                        // 保存选课记录
                        Enrollment enrollment = new Enrollment(studentId, courseId);
                        enrollmentRepository.save(enrollment);
                        return true;
                    } else {
                        // 恢复座位数,因为课程已满
                        redisTemplate.opsForValue().increment("course:" + courseId + ":seats", 1);
                        return false;
                    }
                } finally {
                    // 总是在finally块中释放锁,以防止锁泄露
                    redisTemplate.delete(lockKey);
                }
            } else {
                return false;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    EnrollmentRepository.java - 持久层接口用于管理选课记录:

    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface EnrollmentRepository extends JpaRepository<Enrollment, Long> {
        // JPA/JDBC方法用于管理选课记录
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    CourseService中,我们使用Lua脚本来尝试获取课程的锁。如果锁被成功获取(acquiredLocktrue),我们进一步检查学生是否已经选过这门课程。如果没有,我们减少座位数,并且如果座位仍然可用,我们将学生添加到选课集合中并保存选课记录。如果课程已满或者学生已经选过这门课程,我们释放锁并返回false

    请注意,上述代码知识一个思路演示,在生产系统中,还需要处理各种边缘情况和潜在的异常。可能还需要适当配置StringRedisTemplateEnrollmentRepository,包括在Spring Boot应用程序中设置必要的依赖和注解。
    此外,用于锁定和跟踪学生请求的Redis键需要精心设计,以避免冲突,并确保它们可以被轻松管理和在不再使用后清理。

  • 相关阅读:
    静态Vxlan多活网关实验配置(集中式网关)
    2.类与对象 拜访对象村
    jumpserver堡垒机/跳板机
    建模杂谈系列177 APIFunc继续实践-比对研究
    结合Mockjs与Bus事件总线搭建首页导航和左侧菜单
    springboot+电影售票小程序 毕业设计-附源码201532
    10.DesignForSymbols\QuickSearchSymbol
    Python与pycharm-----windows安装与运行
    开发工具介绍
    【java学习】访问权限修饰符(24)
  • 原文地址:https://blog.csdn.net/weixin_37519752/article/details/138636385