• 项目的一些难点


    1.不用redis?分布式锁,如何防止用户重复点击?

    1.乐观锁

    乐观锁是一种在数据库层面上避免并发冲突的机制。它通常通过在数据库记录中添加一个版本号(或时间戳)来实现。每次更新记录时,都会检查版本号是否与数据库中的版本号匹配,如果匹配,则更新数据并将版本号加一。这确保了在更新期间没有其他操作更改了记录。

    • 应用场景:适用于更新操作并不频繁,且冲突概率较低的场景

    代码实现:

    1. //实体类:注意@Version注解
    2. @Entity
    3. @Table(name = "form_submissions")
    4. public class FormSubmission {
    5. @Id
    6. @GeneratedValue(strategy = GenerationType.IDENTITY)
    7. private Long id;
    8. @Column(name = "user_id")
    9. private Long userId;
    10. @Column(name = "form_data")
    11. private String formData;
    12. @Column(name = "version")
    13. @Version
    14. private int version;
    15. // 构造函数、Getter和Setter略去
    16. }
    17. //业务层
    18. @Service
    19. public class FormSubmissionService {
    20. @Autowired
    21. private FormSubmissionRepository repository;
    22. @Transactional
    23. public String submitForm(Long userId, String formData) {
    24. Optional existingSubmission = repository.findById(userId);
    25. FormSubmission submission;
    26. if (existingSubmission.isPresent()) {
    27. // 更新现有记录
    28. submission = existingSubmission.get();
    29. submission.setFormData(formData);
    30. } else {
    31. // 创建新的记录
    32. submission = new FormSubmission();
    33. submission.setUserId(userId);
    34. submission.setFormData(formData);
    35. submission.setVersion(0); // 或根据需要设置初始版本号
    36. }
    37. try {
    38. repository.save(submission);
    39. return "表单提交成功。";
    40. } catch (org.springframework.orm.ObjectOptimisticLockingFailureException e) {
    41. // 捕获乐观锁异常,处理冲突
    42. return "提交失败,请不要重复提交。";
    43. }
    44. }
    45. }
    46. //没有注解的时候sql层
    47. UPDATE form_submissions
    48. SET form_data = '新的表单数据', version = version + 1
    49. WHERE id = ? AND version = ?;

    2.数据库悲观锁

    悲观锁通常通过数据库提供的锁机制实现,如 SQL 的 SELECT FOR UPDATE 语句,这会锁定被选中的数据库行,直到事务完成。这种方法适用于高冲突环境,因为它会阻止其他任何尝试修改这些行的操作。

    • 应用场景:适用于更新操作频繁,且冲突概率高的场景。

    代码实现:

    1. @Service
    2. public class FormSubmissionService {
    3. @Autowired
    4. private FormSubmissionRepository repository;
    5. @Transactional
    6. public boolean submitForm(Long userId, String formData) {
    7. Optional existingSubmission = repository.findByUserIdForUpdate(userId);
    8. if (existingSubmission.isPresent()) {
    9. // 存在记录,处理重复提交逻辑
    10. return false;
    11. } else {
    12. // 不存在记录,保存新的表单提交
    13. FormSubmission submission = new FormSubmission();
    14. submission.setUserId(userId);
    15. submission.setFormData(formData);
    16. repository.save(submission);
    17. return true;
    18. }
    19. }
    20. }
    21. //sql层代码
    22. @Lock(LockModeType.PESSIMISTIC_WRITE)
    23. @Query("SELECT fs FROM FormSubmission fs WHERE fs.userId = :userId")
    24. Optional findByUserIdForUpdate(@Param("userId") Long userId);

    3.基于内存的锁

    如果你的应用程序运行在单个实例或能够使用共享内存系统(如 Hazelcast、Apache Ignite),可以使用内存中的数据结构来实现锁逻辑。例如,使用一个全局哈希表存储正在进行的操作的标识符,来防止重复。

    • 应用场景:适用于单实例应用或者有共享内存系统的分布式应用。
    1. @Service
    2. public class FormSubmissionService {
    3. private final Map userLocks = new HashMap<>();
    4. public String submitForm(Long userId, String formData) {
    5. Lock lock = userLocks.computeIfAbsent(userId, k -> new ReentrantLock());
    6. if (lock.tryLock()) {
    7. try {
    8. // 模拟表单处理逻辑
    9. Thread.sleep(1000); // 假设处理需要一段时间
    10. System.out.println("表单数据处理: " + formData);
    11. return "表单提交成功。";
    12. } catch (InterruptedException e) {
    13. Thread.currentThread().interrupt();
    14. return "表单处理中断。";
    15. } finally {
    16. lock.unlock();
    17. }
    18. } else {
    19. return "正在处理中,请不要重复提交。";
    20. }
    21. }
    22. }

    4.应用程序级的去重逻辑

    在应用程序级别实现去重逻辑,例如,通过在前端禁用提交按钮,直到请求完成,或者在后端设置一个短暂的时间窗口,在这个窗口内忽略来自同一用户的重复请求。

    • 应用场景:适用于需要快速实现且冲突概率不高的场景。

    5.唯一标识符

    要求客户端在请求时生成一个唯一的标识符(如 UUID),并在服务器端检查这个标识符是否已经被处理。这个标识符可以存储在内存或数据库中,以确保每个请求只被处理一次。

    • 应用场景:适合于任何需要确保请求唯一性的场景,特别是在分布式系统中。

    代码实现:

    1. @Service
    2. public class RequestService {
    3. @Autowired
    4. private RequestIdRepository requestIdRepository;
    5. public boolean processRequest(String requestId) {
    6. // 检查请求ID是否已存在
    7. Optional existingRequestId = requestIdRepository.findById(requestId);
    8. if (existingRequestId.isPresent()) {
    9. // 请求ID已存在,拒绝重复处理
    10. return false;
    11. } else {
    12. // 请求ID不存在,处理请求
    13. RequestId newRequestId = new RequestId();
    14. newRequestId.setId(requestId);
    15. requestIdRepository.save(newRequestId); // 保存请求ID标记为已处理
    16. // 在这里执行其他请求处理逻辑...
    17. return true;
    18. }
    19. }
    20. }

    在这个实现中,客户端需要生成一个 UUID 并在每次请求时发送这个 UUID 作为 `requestId` 参数。服务器通过检查这个 `requestId` 是否已经存在于 `request_ids` 表中来防止重复处理相同的请求。这种方法适用于分布式系统中确保请求的唯一性,有效地防止了用户因为多次点击导致的重复请求问题。

    2.Cookie、Session、Token、JWT之间的区别

    傻傻分不清之 Cookie、Session、Token、JWT - 掘金

    3.40亿个QQ号,限制1G内存,如何去重?

    1.位图(Bitmap)

    图是一种非常高效的数据结构,通过使用1个位来标记某个元素是否存在。假设我们使用40亿位(或者说500MB)的内存空间,就可以表示40亿个不同的QQ号。

    1. 初始化位图:创建一个大约500MB大小的位图,每个位对应一个可能的QQ号。由于QQ号可能不会完全连续,我们需要根据实际QQ号的范围来调整位图的大小。

    2. 标记QQ号:遍历所有QQ号,对每个QQ号,计算它在位图中的位置,并将相应的位设置为1。

    3. 去重:再次遍历QQ号,通过检查位图中对应位的值,可以判断一个QQ号是否已经出现过。

    这种方法的缺点是,如果QQ号的范围非常大,位图的大小可能会超出1GB的内存限制。

    代码实现:

    1. public class Bitmap {
    2. private byte[] bits;
    3. public Bitmap(int size) {
    4. bits = new byte[(size + 7) / 8];
    5. }
    6. public void set(int k) {
    7. int byteIndex = k / 8;
    8. int bitIndex = k % 8;
    9. bits[byteIndex] |= (1 << bitIndex);
    10. }
    11. public boolean get(int k) {
    12. int byteIndex = k / 8;
    13. int bitIndex = k % 8;
    14. return (bits[byteIndex] & (1 << bitIndex)) != 0;
    15. }
    16. }
    17. // 假设QQ号范围在一定区间内,这里简化处理
    18. Bitmap bitmap = new Bitmap(4000000000); // 大约需要500MB内存
    19. // 设置QQ号
    20. bitmap.set(qqNumber);
    21. // 检查QQ号是否已存在
    22. boolean exists = bitmap.get(qqNumber);

    2.布隆过滤器(Bloom Filter)

    布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否在集合中:

    1. 初始化布隆过滤器:根据数据量和可接受的误判率初始化布隆过滤器。

    2. 添加元素:遍历QQ号,将每个QQ号添加到布隆过滤器中。

    3. 预过滤:再次遍历QQ号,首先使用布隆过滤器检查是否可能已经存在。由于布隆过滤器存在一定的误判率,对于判断存在的元素,需要进一步确认。

    布隆过滤器适用于快速预过滤,减少需要进一步处理的数据量,但需要额外的机制来处理误判。

    1. public class BloomFilter {
    2. private BitSet hashes;
    3. private int size;
    4. public BloomFilter(int size) {
    5. this.size = size;
    6. this.hashes = new BitSet(size);
    7. }
    8. private int hash(Object obj, int k) {
    9. return Math.abs(obj.hashCode() * k) % size;
    10. }
    11. public void add(Object obj) {
    12. for (int k = 1; k <= 3; k++) { // 使用3个不同的哈希函数
    13. int hash = hash(obj, k);
    14. hashes.set(hash);
    15. }
    16. }
    17. public boolean mightContain(Object obj) {
    18. for (int k = 1; k <= 3; k++) {
    19. int hash = hash(obj, k);
    20. if (!hashes.get(hash)) {
    21. return false; // 如果有一个位不是1,那么对象肯定没有添加
    22. }
    23. }
    24. return true; // 可能包含
    25. }
    26. }
    27. // 使用
    28. BloomFilter filter = new BloomFilter(100000000); // 需要调整大小以适应内存限制
    29. filter.add(qqNumber);
    30. boolean mightExist = filter.mightContain(qqNumber);

    4.假如博主有上百万个粉丝关注,那么怎么保证正确而又完整地通知到用户呢?

    在面对上百万粉丝关注的场景下,确保每个用户都能正确而完整地接收到通知是一个挑战。这个问题通常出现在社交网络、内容分发平台、即时通讯服务等系统中。为了应对这种挑战,通常会采用"推拉结合"(Push-Pull Hybrid)的模式来优化信息分发机制。下面是对"推拉结合"策略及其在feed流系统中应用的解释:

    推拉结合(Push-Pull Hybrid)模式

    推送(Push)机制
    • 实时性:系统主动将新内容推送给用户,适用于需要高实时性的场景,如即时通讯。
    • 资源消耗:对于有大量粉丝的博主,一次性推送会消耗大量的服务器资源和网络带宽。
    • 适用场景:适合对实时性要求较高的通知,比如好友消息、直播开始通知等。
    拉取(Pull)机制
    • 按需获取:用户主动请求(拉取)新内容,适用于不需要即时了解最新信息的场景。
    • 资源节约:服务器只在用户请求时才发送数据,减少了资源的浪费。
    • 适用场景:适合用户浏览feed流、查看更新列表等场景,用户可以在自己方便的时候拉取最新内容。

    推拉结合策略在Feed流中的应用

    1. 初始内容加载使用拉取:用户打开应用时,通过拉取机制加载feed流的初始内容。这样做可以减少服务器压力,因为内容加载是基于用户的主动请求。

    2. 实时更新使用推送:对于新内容的更新,特别是用户关注的博主有新动态时,可以采用推送机制通知用户。为了减少资源消耗,可以采取一些优化措施:

      • 分批推送:将粉丝分成不同的批次,逐批推送更新,避免一次性对服务器造成过大压力。
      • 仅推送通知:而不是推送全部新内容,只通知用户有新内容,用户点击通知后再通过拉取获取具体内容。
      • 使用消息队列:利用消息队列管理推送任务,可以平衡负载,提高系统的扩展性和可靠性。
    3. 智能化推拉结合:系统可以根据用户的行为、偏好以及网络状况智能选择推送或拉取。例如,对于经常互动的粉丝使用推送,对于不活跃的用户则在其主动打开应用时通过拉取更新。

    4. 后台拉取:在某些应用场景中,可以在应用处于后台时做轻量级的数据拉取,保证用户回到应用时能看到较新的内容,同时不过度消耗用户的数据流量和电量。

    推拉结合模式通过平衡系统资源消耗和用户体验的实时性,为处理大规模用户通知提供了一种可行且有效的解决方案。在实际应用中,根据具体的业务需求和用户行为,灵活调整推送和拉取的策略至关重要。

  • 相关阅读:
    RPC 核心原理理论分析
    Linux 错误码
    使用OpenResty+Lua实现灰度测试(金丝雀)
    Java中使用MyBatis框架连接和操作MySQL数据库
    后台管理-----内容区域表单优化(针对页面不同数据的处理)
    sparkctl x86/arm不同平台编译使用
    用DIV+CSS技术设计的音乐主题网站(web前端网页制作课作业)
    C语言【空字符】和【空指针】
    【SpringCloud原理】万字剖析OpenFeign之FeignClient动态代理生成源码
    二叉树的层序遍历
  • 原文地址:https://blog.csdn.net/weixin_53693850/article/details/136182268