java用户签到的几种实现方法以及性能优化

   日期:2024-12-26    作者:rfljzz00 移动:http://oml01z.riyuangf.com/mobile/quote/51668.html
一.方案设计
后端方案-基于数据库

在数据库中设计一张签到表,记录用户每次签到的日期及其他相关信息。然后通过时间范围查询得到用户的签到记

录示例表结构如下: 

 

通过唯一索引,可以确保同一用户在同一天内只能签到一次。通过下面的 SQL 即可查询用户的签到记录:

java代码:

实现简单这里不做演示

优点:原理简单,容易实现,适用于用户量较小的系统。

缺点:随着用户量和数据量增大,对数据库的压力增大,直接查询数据库性能较差。除了单接口的响应会增加,可能整个系统都会被其拖垮。

试想一下,每天1万个用户签到,1 个月就 30 万条数据,3 个月就接近百万的数据量了,占用硬盘空间大概 50 MB。存储 100 万个用户 365 天的签到记录,需要 17.52 GB 左右。

后端方案-基于缓存 Redis Set

可以利用内存缓存加速读写,常用的本地缓存是 Caffeine,分布式缓存是 Redis。由于每个用户会有多个签到记

,很适合使用 Redis 的 Set 类型存储,每个用户对应一个键,Set 内的每个元素为签到的具体日期。

Redis Key 的设计为:user:signins: {userId}

其中:

  • user是业务领域前缀
  • siqnins 是具体操作或功能
  • {userld} 表示每个用户,是动态值

如果 Redis 被多个项日公用,还可以在开头增加项目前缀区分,比如 mianshiniu:user:signins:fuserId}。

扩展知识:Redis 键设计规范

  • 明确性: 键名称应明确表示数据的含义和结构。例如,通过使用 signins 可以清楚地知道这个键与用户的签到记录有关。
  • 层次结构: 使用冒号 :分隔不同的部分,可以使键结构化,便于管理和查询
  • 唯一性: 确保键的唯一性,避免不同数据使用相同的键前缀。
  • 一致性: 在整个系统中保持键设计的一致性,使得管理和维护变得更加简单。
  • 长度: 避避免过长的键名称,以防影响性能和存储效率。.

具体示例如下,可以使用 Redis 命令行工具添加值到集合中:

 

使用命令查找集合中的值

 

该万案的

优点: Set 数据结构天然支持去重,适合存储和检索打卡记录

缺点: 上述设计显然存储了很多重复的字符串,针对海量数据场景,需要考虑内存的占用量。

 

其中,年份被重复存储。

为了减少内存占用,还可以在 key 中增加更多日期层级,比如 user:signins:{year}:{userId}。示例命令如下:

 

这样一来,不仅节约了内存,也便于管理,可以轻松查询某个用户在某个年份的签到情况。 存储 100 万个用户的 365 天 签到记录,使用 Redis 集合类型来存储每个用户的签到信息,每个用户需要大约 1880字节 的空间,总共需要大约 1.88GB 的内存空间,相比数据库节约了 10 倍左右。

有没有更节约内存的方式呢?

后端方案Redis Bitmap 位图(推荐)

Bitmap 位图,是一种使用位(bit)来表示数据的 紧凑 数据结构。每个位可以存储两个值:0 或 1,常用于表示某种

状态或标志。因为每个位仅占用1位内存,Bitmap 在大规模存储二值数据(如布尔值)时,非常高效且节约空间。

核心思想: 与其存储用户签到的具体日期,不如存储用户在今年的第 N 天是否签到。

 

使用位图类型存储,每个用户对应一个键,Bitmap 的 每一位 来表示用户在 某一天 是否打卡。

举个例子,我们签到的状态可以用0和1表示,0代表未签到,1代表到。 从后往前看

 

而 int 类型占用的空间为4个字节(byte),一个字节占8位(bit),即一个 int 占 32 位。在这种仅存储二值(0 或 1)的场景,就可以使用 Bitmap 位图来优化存储,因为一个 bit 就可以表示0和 1。把 int 优化成用 bit 存储,那么占用的空间可以优化 32 倍!假设原先占用的大小需要 32G,那么改造后仅需1G。

这里需要注意:

现代计算机体系结构通常以字节(8位)作为最小寻址单位,那么上述的 bit 是如何存储的呢?

答案就是 打包.

通过将多个 bit 打包到一个字节(或者其他更大的数据类型,如 int、long)中来实现的。每个字节(或数据类型)被视

为一个桶,里面可以存放若干个布尔值(0 或 1)。

对每一位操作时,要使用位运算进行访问

对于刷题签到记录场景,一个用户存储一年的数据仅需占用 46 字节,因为 46*8= 368,能覆盖 365 天的记录。那一百万用户也才占用 43.8 MB,相比于 Redis set 结构节约了 40 多倍存储空间!

1000w 个用户也才占用 438 MB! 

当然,我们没必要自己通过 int 等类型实现 Bitmap,JDK 自带了 BitSet类、Redis 也支持 Bitmap 高级数据结构。考虑到项目的分布式、可扩展性,采用 Redis 的 Bitmap 实现。

Redis Key 的设计为: user:signins:{年份}:{userId}

在 Java 程序中,还可以使用 Redisson 库提供的现成的 RBitSet,对应的redis也就是Bitmap 开发成本也很低.

这种方案

  • 优点: 内存占用极小,适合大规模用户和日期的场景。
  • 缺点 :需要熟悉位图操作,不够直观。

总结一下:

  • 基于性能的考虑,我们选用 Redis 中间件来存储用户的签到记录
  • 基于空间的考虑,我们选用 Bitmap 数据结构来存储用户的签到记录
二.java实现
2.1签到接口实现

1 在 config 目录下编写 Redisson 客户端配置类,会自动读取项目中的 Redis 配置,初始化客户端 Bean。

 

2 添加刷题签到记录接口

接口逻辑:判断目前用户当天是否签到

如果已签到,则忽略

如果未签到,则在 Bitmap 中设置记录

1)因为读写 Redis 使用的是相同的 key,可以将所有 Redis 的 key 单独定义成常量,放在 constant 日录下,还可以提供拼接完整 key 的方法。代码如下:

 

Userservice编写接口

 

实现类

 

controller

 
2.2查询用户是否签到实现

实现思路:

1.通过 userld 和当前年份从 Redis 中获取对应的 Bitmap

2.获取当前年份的总天数

3.循环天数拼接日期,根据日期去 Bitmap 中判断是否有签到记录,并记录到数组中

4.最后,将拼接好的、一年的签到记录返回给前端即可

1)在 UserService 中定义接口:

 

2)编写实现类

 
 

为什么使用了 LinkedHashMap 而不使用HashMap

因为LinkedHashMap保证了键值对映射的有序性,相当于直接得到了映射列表,符合前端要求的返回值格式。

而HashMap不保证元素的顺序,因为插入顺序和遍历顺序可能不同.

3) 编写Controller代码

 

4) 通过 Swagger 接口文档调用接口进行测试即可 使用JMeter进行压测(放最后测试了)

优化2.2查询接口

1 返回值优化

从示例结果我们可以看到 传输的数据较多、计算时间耗时、带宽占用多、效率低。

修改代码如下:

 

3、计算优化

上述代码中,我们使用循环来遍历所有年份,而循环是需要消耗 CPU 计算资源的。在Java 中的 Bitset 类中,可以使用 nextsetBit(int fromIndex)和 nextclearBit(int fromIndex)方法来获取从指定索引开始的下一个 已设置(即为 1)或 未设置(即为 0)的位。主要是2 个方法:

nextsetBit(int fromIndex):从 fromIndex 开始(包括 fromIndex 本身)寻找下一个被设置为1的位。如果找到了,返回该位的索引;如果没有找到,返回-1。

nextclearBit(int fromIndex):从 fromIndex 开始(包括 fromIndex 本身)寻找下一个为0的位。如果找到了返回该位的索引:如果没有找到,返回一个大的整数值。

使用 nextSetBit,可以跳过无意义的循环检查,通过位运算来获取被设置为1的位置,性能更高。

修改后的代码如下:

 
测试

优化前

使用JMeter压测1秒钟执行200次请求,吞吐量只有73左右

性能优化后

使用JMeter压测1秒钟执行200次请求,吞吐量可达200左右

 如有写的不对的地方欢迎指正交流


特别提示:本信息由相关用户自行提供,真实性未证实,仅供参考。请谨慎采用,风险自负。


举报收藏 0评论 0
0相关评论
相关最新动态
推荐最新动态
点击排行
{
网站首页  |  关于我们  |  联系方式  |  使用协议  |  隐私政策  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报  |  鄂ICP备2020018471号