非常全面的Java SpringBoot点赞功能实现

 更新时间:2022年01月23日 11:02:28   作者:JoeyHua  
但是这些功能再项目中是高频出现的,如果直接操作数据库的话,对数据库压力太大。那遇到这个问题怎么解决?这篇文章主要给大家介绍了关于Java SpringBoot点赞功能实现 的相关资料,需要的朋友可以参考下

前言

最近公司在做一个NFT商城的项目,大致就是一个只买卖数字产品的平台,项目中有个需求是用户可以给商品点赞,还需要获取商品的点赞总数,类似下图

起初感觉这功能很好实现,无非就是加个点赞表嘛,后来发现事情并没有这么简单。

一开始的设计是这样的,一共有三张表:商品表、用户表、点赞表,用户点赞的时候把用户id和商品id加到点赞表中,并给对应的商品点赞数+1。看起来没什么问题,逻辑也比较简单,但是测试的时候缺发现了奇怪的bug,点赞数量有时候会不正确,结果会比预期的大。

下面贴下关键代码(项目使用了Mybatis-Plus):

public boolean like(Integer userId, Integer productId) {
        // 查询是否有记录,如果有记录直接返回
        Like like = getOne(new QueryWrapper<Like>().lambda()
                .eq(Like::getUserId, userId)
                .eq(Like::getProductId, productId));
        if(like != null) {
            return true;
        }

        // 保存并商品点赞数加1
        save(Like.builder()
                .userId(userId)
                .productId(productId)
                .build());
        return productService.update(new UpdateWrapper<Product>().lambda()
                .setSql("like_count = like_count + 1")
                .eq(Product::getId, productId));
}

看上去没什么问题,但是测试后数据却不正确,为什么呢?

实际上这是一个并发问题,只要在并发的情况下就会出现问题,我们知道Spring Mvc是基于servlet的,servlet在接收到用户请求后会从线程池中拿一个线程分配给它,每个请求都是一个单独的线程。试想一下,如果A线程在执行完查询操作后,发现没有记录,随后由于CPU调度,把控制权让了出去,然后B线程执行查询,也发现没有记录,这时候A和B线程都会执行保存并商品点赞数加1这个操作,导致数据不正确。

CPU操作顺序:A线程查询 -> B线程查询 -> A线程保存 -> B线程保存

下面使用JMeter模拟一下并发的情况,模拟用户在1秒内对商品执行100次点赞请求,结果应该是1,但得到的结果却是28(实际结果不一定是28,可能是任何数字)。

解决方案

青铜版

使用synchronized关键字锁住读写操作,操作完成后释放锁

public boolean like(Integer userId, Integer productId) {
        String lock = buildLock(userId, productId);
        synchronized (lock) {
            // 查询是否有记录,如果有记录直接返回
            Like like = getOne(new QueryWrapper<Like>().lambda()
                    .eq(Like::getUserId, userId)
                    .eq(Like::getProductId, productId), false);
            if(like != null) {
                return true;
            }

            // 保存并商品点赞数加1
            save(Like.builder()
                    .userId(userId)
                    .productId(productId)
                    .build());
            return productService.update(new UpdateWrapper<Product>().lambda()
                    .setSql("like_count = like_count + 1")
                    .eq(Product::getId, productId));
        }
}

private String buildLock(Integer userId, Integer productId) {
        StringBuilder sb = new StringBuilder();
        sb.append(userId);
        sb.append("::");
        sb.append(productId);
        String lock = sb.toString().intern();

        return lock;
}

这里要注意一点,使用String作为锁时一定要调用intern()方法,intern()会先从常量池中查找有没有相同的String,如果有就直接返回,没有的话会把当前String加入常量池,然后再返回。如果不调用这个方法锁会失效。

JMeter性能数据

优点:

保证了正确性

缺点:

性能太差,并发低的情况下还可以应付,并发高时用户体验极差

白银版

点赞表user_id和product_id加上联合索引,并使用try catch捕获异常,防止报错。由于使用了联合索引,所以不需要在新增前查询了,mysql会帮我们做这件事。

public boolean like(Integer userId, Integer productId) {
        try {
            // 保存并商品点赞数加1
            save(Like.builder()
                    .userId(userId)
                    .productId(productId)
                    .build());
            return productService.update(new UpdateWrapper<Product>().lambda()
                    .setSql("like_count = like_count + 1")
                    .eq(Product::getId, productId));
        }catch (DuplicateKeyException exception) {

        }

        return true;
}

JMeter性能数据

优点:

性能比上一个方案好

缺点:

中规中矩,没什么大的缺点

黄金版

使用Redis缓存点赞数据(点赞操作使用lua脚本实现,保证操作的原子性),然后定时同步到mysql。

注意:Redis需要开启持久化,最好aof和rdb都开启,不然重启数据就丢失了

public boolean like(Integer userId, Integer productId) {
        List<String> keys = new ArrayList<>();
        keys.add(buildUserRedisKey(userId));
        keys.add(buildProductRedisKey(productId));

        int value1 = 1;

        redisUtil.execute("lua-script/like.lua", keys, value1);

        return true;
}

private String buildUserRedisKey(Integer userId) {
        return "userId_" + userId;
}

private String buildProductRedisKey(Integer productId) {
        return "productId_" + productId;
}

lua脚本

local userId = KEYS[1]
local productId = KEYS[2]
local flag = ARGV[1] -- 1:点赞 0:取消点赞


if flag == '1' then
  -- 用户set添加商品并商品点赞数加1
  if redis.call('SISMEMBER', userId, productId) == 0 then
    redis.call('SADD', userId, productId)
    redis.call('INCR', productId)
  end
else
  -- 用户set删除商品并商品点赞数减1
  redis.call('SREM', userId, productId)
  local oldValue = tonumber(redis.call('GET', productId))
  if oldValue and oldValue > 0 then
    redis.call('DECR', productId)
  end
end

return 1

JMeter性能数据

优点:

  • 性能非常好

缺点:

  • 数据量多了内存占用较高总结

如果对性能没有要求,可以使用白银版的实现方式,如果有要求,就使用黄金版的方式,内存占用大的问题也可以通过一些手段来解决,比如可以根据业务需求定期删除一些不常用的缓存数据,但是相对应的,查询的时候就需要在查询失败时再去查数据库。

源码

源码地址:https://github.com/huajiayi/like-demo

源码里有一些功能没有实现,比如定时同步功能,需要根据业务需求自行实现

总结

到此这篇关于Java SpringBoot点赞功能实现的文章就介绍到这了,更多相关Java SpringBoot点赞功能内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C++和Java命令行绘制心形图案

    C++和Java命令行绘制心形图案

    这篇文章主要为大家详细介绍了C++和Java命令行绘制心形图案,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-04-04
  • 细品Java8中hashCode方法的使用

    细品Java8中hashCode方法的使用

    这篇文章主要介绍了细品Java8中hashCode方法的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • Maven添加Tomcat插件实现热部署代码实例

    Maven添加Tomcat插件实现热部署代码实例

    这篇文章主要介绍了Maven添加Tomcat插件实现热部署代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • java通过jni调用opencv处理图像的方法

    java通过jni调用opencv处理图像的方法

    今天小编就为大家分享一篇java通过jni调用opencv处理图像的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-08-08
  • SpringBoot实现人脸识别等多种登录方式

    SpringBoot实现人脸识别等多种登录方式

    本文主要介绍了SpringBoot实现人脸识别等多种登录方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • Java多线程并发编程和锁原理解析

    Java多线程并发编程和锁原理解析

    这篇文章主要介绍了Java多线程并发编程和锁原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • spring boot如何基于JWT实现单点登录详解

    spring boot如何基于JWT实现单点登录详解

    这篇文章主要介绍了spring boot如何基于JWT实现单点登录详解,用户只需登录一次就能够在这两个系统中进行操作。很明显这就是单点登录(Single Sign-On)达到的效果,需要的朋友可以参考下
    2019-06-06
  • Java RMI详细介绍及简单实例

    Java RMI详细介绍及简单实例

    这篇文章主要介绍了Java RMI详细介绍及简单实例的相关资料,需要的朋友可以参考下
    2017-02-02
  • SpringBoot如何使用Scala进行开发的实现

    SpringBoot如何使用Scala进行开发的实现

    这篇文章主要介绍了SpringBoot如何使用Scala进行开发的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • MyBatis中的SQL映射文件配置结果映射的操作指南

    MyBatis中的SQL映射文件配置结果映射的操作指南

    MyBatis 是一款优秀的 ORM 框架,它提供了多种配置方式来定义 SQL 语句以及结果映射规则,本文将介绍 MyBatis 中的 SQL 映射文件如何配置结果映射,包括常规类型、集合类型等多种情况,需要的朋友可以参考下
    2023-07-07

最新评论