资讯专栏INFORMATION COLUMN

解决"并发下查询并更新带来的问题"

roadtogeek / 2125人阅读

摘要:场景在日常开发中经常遇到先根据条件判断某条数据是否存在,如果不存在的话就插入,如果存在的话就更新或提示异常。查询名字叫的用户是否存在如果不存在就插入数据使用锁其实和代码块是相同的作用,但是要注意必须在中释放锁,避免出现异常死锁了。

场景:

在日常开发中经常遇到先根据条件判断某条数据是否存在,如果不存在的话就插入,如果存在的话就更新或提示异常。一般代码的模式都写成下面这个样子,是一种很常见的写法,但是在并发情况下很容易会重复插入两条数据,大概的情况就是第一个请求进来,没有查询到该用户通过了if判断,但是if中有比较耗时的逻辑,在第一个请求还没执行insert的时候第二个请求也进来了,因为这个时候第一个请求还没执行insert操作,所以第二个请求也没有查询到该用户也通过了if判断,这个样子就造成了两条重复的数据。

// 查询名字叫user1的用户是否存在
UserVo userVo= userMapper.selectUserByName("user1");
    // 如果不存在就插入数据
    if (userVo==null) {
        Thread.sleep(10000);
        UserVo userVo = new UserVo();
        userVo.setUserName("user1");
        userMapper.insert(userVo);
     }
}
解决方法: 1.使用synchronized同步代码块

直接将查询校验逻辑和插入逻辑都进行同步,也就是说第一个请求的逻辑没结束,第二个请求就会一直等待着,只有当第一个请求执行完同步代码块中的逻辑释放锁后第二个请求才能获取到锁执行这段逻辑。

private Object obj = new Object();

synchronized (object){
    // 查询名字叫user1的用户是否存在
    UserVo userVo= userMapper.selectUserByName("user1");
    // 如果不存在就插入数据
    if (userVo==null) {
        Thread.sleep(10000);
        UserVo userVo = new UserVo();
        userVo.setUserName("user1");
        userMapper.insert(userVo);
     }
}
2.使用Lock

其实和synchronized代码块是相同的作用,但是要注意必须在finally中释放锁,避免出现异常死锁了。

private Lock lock = new ReentrantLock();
try {
    lock.lock();
    // 查询名字叫user1的用户是否存在
    UserVo userVo = userMapper.selectUserByName("user1");
    // 如果不存在就插入数据
    if (userVo == null) {
        Thread.sleep(10000);
        UserVo userVo = new UserVo();
        userVo.setUserName("user1");
        userMapper.insert(userVo);
    }
} finally {
    lock.unlock();
}
3.给数据库索引

既然是要根据用户名字判断是否有重复数据,所以可以直接在数据库上给userName字段添加UNIQUE索引,这样在第二次重复插入的时候就会提示异常。如果不想重复插入的时候有报错提示可以使用INSERT IGNORE INTO语句。而代码则不必做任何逻辑操作。

// 查询名字叫user1的用户是否存在
UserVo userVo= userMapper.selectUserByName("user1");
// 如果不存在就插入数据
if (userVo==null) {
    Thread.sleep(10000);
    UserVo userVo = new UserVo();
    userVo.setUserName("user1");
    userMapper.insert(userVo);
   }
}
4.使用redissetnx来作为锁

redissetnx命令是只有当你存入的key不存在时才会成功存入,并返回1,而如果key已经存在的时候则存入失败并返回0,我们可以拿这个特性来当做锁。首先这个方法进来第一步就是执行setnx操作,把查询的用户名存入redis,然后查询该用户是否存在,第一个请求进到if判断中但是没执行插入逻辑,第二个请求虽然也没有查询到该用户,但是它的setnx会失败,因为第一个请求存的key还没删除,所以这样就避免了并发重新插入的问题,而且最大的优点就是它不像synchronizedLock无论所有请求进来都只能一个一个通过,使用这种方法是只有当操作同一个用户有并发请求的时候才会阻塞,而如果是请求两个不同的用户时是不会阻塞的,都可以顺利通过,因为存入的key是不同的。

// 自动注入spring的redis操作类
@Autowired
private RedisTemplate redisTemplate;

public String addUser (String userName) {
    // 执行setnx命令,存入当前拿来判断的用户名
    BoundValueOperations operations = redisTemplate.boundValueOps(userName);
    // 执行setnx命令的结果,这里封装的方法是直接返回true和false
    boolean addFlag = operations.setIfAbsent(1);

    // 返回结果
    String result = null;
    
    UserVo userVo= userMapper.selectUserByName(userName);
    try {
        if (userVo == null && addFlag == true) {
            Thread.sleep(10000);
            UserVo userVo = new UserVo();
            userVo.setUserName("user1");
            userMapper.insert(userVo);
            result = "更新成功";
        } else{
            result = "更新失败";
        }
    } finally {
        // 无论更新成功和失败都去删除setnx添加的key
        operations.getOperations().delete(userName);
    }
    return result;
}
总结:

上述四种方法,给数据库加索引、Lockredis都有使用过,synchronizedLock也差不多,个人感觉给数据库加索引来控制这种并发太死板了,万一系统中有其他地方的逻辑是需要重复添加这个字段的数据,这个时候就没办法使用索引了,synchronizedLock效率太低了,如果是并发量太大的这种方式肯定是不可缺的,而redis的这种方法则效率高很多,比较适合并发量高的操作。

结尾:

因为本人接触的系统的并发量也不是很大,所以对这方面的技术也是自己在钻研摸索,可能会有很多地方有遗漏和错误,如果大家有更好的方法欢迎一起留言讨论,也欢迎指出错误。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77799.html

相关文章

  • 支付宝防发方案之"一锁二判三更新"

    摘要:每年支付宝在双和双的活动中,都展示了绝佳的技术能力。对于异步并发重复消息的处理亦是如此,加深对状态机的判断后还可以处理消息乱序问题。 每年支付宝在双11和双12的活动中,都展示了绝佳的技术能力。这个能力不但体现在处理高TPS量的访问,更体现在几乎不会出错,不会出现重复支付的情况,那这个是怎么做到的呢? 诚然,为了实现在高并发下仍不会出错的技术目标,支付宝下了很多功夫,比如幂等性的处理,...

    imingyu 评论0 收藏0
  • 支付宝防发方案之"一锁二判三更新"

    摘要:每年支付宝在双和双的活动中,都展示了绝佳的技术能力。对于异步并发重复消息的处理亦是如此,加深对状态机的判断后还可以处理消息乱序问题。 每年支付宝在双11和双12的活动中,都展示了绝佳的技术能力。这个能力不但体现在处理高TPS量的访问,更体现在几乎不会出错,不会出现重复支付的情况,那这个是怎么做到的呢? 诚然,为了实现在高并发下仍不会出错的技术目标,支付宝下了很多功夫,比如幂等性的处理,...

    yibinnn 评论0 收藏0
  • python查询本身拼装所有库导出命令

      文章内容通常是阐述了python查询本身拼装所有库并导出,主要包括查询拼装库依据命令查询,导出库安装文件运行指令,原文中给大家介绍得相当详细,对于大家学习与工作具有极强的参考文献参照实际意义,务必的朋友可以参考一下  一、查询拼装库  1.命令查询  piplist  2.从安装路径site-packages查询  二、导出库安装文件  1.导出  在我们要导出的库文件夹内运行指令:  pip...

    89542767 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<