由于用户同时访问线上的下订单接口,导致在扣减库存时出现了异常,这是一个很典型的并发问题,本篇文章为解决并发问题而生,采用的技术为Redis锁机制+多线程的阻塞唤醒方法。

在实现Redis锁机制之前,我们需要了解一下前置知识。

一、前置知识

1、多线程

将wait()、notifyAll()归为到多线程的方法中略有一些不恰当,这两个方法是Object中的方法。

① 当调用了wait()方法后,让当前线程进入等待状态,并且让当前线程释放对象锁,等待既为阻塞状态,等待notifyAll()方法的唤醒。

wait()方法和sleep()方法有一些相似之处,都是使当前线程阻塞,但他们实际是有一些区别的。

  1. 执行wait() 方法之前需要请求锁,wait()方法执行的时候会释放锁,等待被唤醒的时候竞争锁。
  2. sleep()只是让当前线程休眠一段时间,无视锁的存在。
  3. wait() 是Object类的方法 sleep()是Thread的静态方法

② notifyAll()方法为唤醒wait()中的线程。

notifyAll() 和 notify() 方法都是可以唤醒调用了wait()方法,而陷入阻塞的线程。

但是notify()是随机唤醒这个阻塞队列中随机的一个线程,而notifyAll()是唤醒所用的调用了wait()方法而陷入阻塞的线程,让他们自己去抢占对象锁。

notifyAll() 和 notify() 也都是必须在加锁的同步代码块中被调用,它们起的是唤醒的作用,不是释放锁的作用,只用在当前同步代码块中的程序执行完,也就是对象锁自然释放了,notifyAll() 和 notify()方法才会起作用,去唤醒线程。

wait()方法一般是和notify() 或者 notifyAll() 方法一起连用的。

以上为掌握本篇博客必备的多线程知识,如果系统学习多线程的相关知识可查阅博客 程序员田同学

2、Redis

加锁的过程本质上就是往Redis中set值,当别的进程也来set值时候,发现里面已经有值了,就只能放弃获取稍后再试。

Redis提供了一个天然实现锁机制的方法。

  1. Redis客户端的命令为 setnx(set if not exists)

在集成Springboot中采用的方法为:

  1. redisTemplate.opsForValue().setIfAbsent(key, value);

如果里面set值成功会返回True,如果里面已经存在值就会返回False。

在我们实际使用的时候,setIfAbsent()方法并不是总是返回True和False。

如果我们的业务中加了事务,该方法会返回null,不知道这是一个bug还是什么,这是Redis的一个巨坑,浪费了很长时间才发现了这个问题,如果解决此问题可以跳转到第四章。

二、实现原理

分布式锁本质上要实现的目标就是在 Redis 里面占一个位置,当别的进程也要来占时,发现已经有人占在那里了,就只好放弃或者稍后再试。占位一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占位。先来先占, 事办完了,再调用 del 指令释放茅坑。

其中,发现Redis中已经有值了,当前线程是直接放弃还是稍后再试分别就代表着,非阻塞锁和阻塞锁。

在我们的业务场景中肯定是要稍后再试(阻塞锁),如果是直接放弃(非阻塞锁)在数据库层面就可以直接做,就不需要我们在代码大费周章了。

非阻塞锁只能保存数据的正确性,在高并发的情况下会抛出大量的异常,当一百个并发请求到来时,只有一个请求成功,其他均会抛出异常。

Redis非阻塞锁和 MySQL的乐观锁,最终达到的效果是一样的,乐观锁是采用CAS的思想。

乐观锁方法:表字段 加一个版本号,或者别的字段也可以!加版本号,可以知道控制顺序而已!在update 的时候可以where后面加上version= oldVersion。数据库,在任何并发的情况下,update 成功就是 1 失败就是 0 .可以根据返回的 1 ,0 做相应的处理!

我们更推荐大家使用阻塞锁的方式。

当获取不到锁时候,我们让当前线程使用wait()方法唤醒,当持有锁的线程使用完成后,调用notifyAll()唤醒所有等待的方法。

三、具体实现

以下代码为阻塞锁的实现方式。

业务层:

  1. public String test() throws InterruptedException {
  2. lock("lockKey");
  3. System.out.println("11");
  4. System.out.println("22");
  5. System.out.println(Thread.currentThread().getName()+"***********");
  6. Thread.sleep(2000);
  7. System.out.println("33");
  8. System.out.println("44");
  9. System.out.println("55");
  10. unlock("lockKey");
  11. return "String";
  12. }

锁的工具类:

主要是加锁和解锁的两个方法。

  1. //每一个redis的key对应一个阻塞对象
  2. private static HashMap<String, Object> blockers = new HashMap<>();
  3. //当前获得锁的线程
  4. private static Thread curThread;
  5. public static RedisTemplate redisTemplate = (RedisTemplate) SpringUtils.getBean("redisTemplate") ;
  6. /**
  7. * 加锁
  8. * @param key
  9. * @throws InterruptedException
  10. */
  11. public static void lock(String key) {
  12. //循环判断是否能够创建key, 不能则直接wait释放CPU执行权
  13. //放不进指说明锁正在被占用
  14. System.out.println(key+"**");
  15. while (!RedisUtil.setLock(key,"1",3)){
  16. synchronized (key) {
  17. blockers.put(key, key);
  18. //wait释放CPU执行权
  19. try {
  20. key.wait();
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }
  26. blockers.put(key, key);
  27. //能够成功创建,获取锁成功记录当前获取锁线程
  28. curThread = Thread.currentThread();
  29. }
  30. /**
  31. * 解锁
  32. * @param key
  33. */
  34. public static void unlock(String key) {
  35. //判断是否为加锁的线程执行解锁, 不是则直接忽略
  36. if( curThread == Thread.currentThread()) {
  37. RedisUtil.delete(key);
  38. //删除key之后需要notifyAll所有的应用, 所以这里采用发订阅消息给所有的应用
  39. // RedisUtil.publish("lock", key);
  40. //notifllall其他线程
  41. Object lock = blockers.get(key);
  42. if(lock != null) {
  43. synchronized (lock) {
  44. lock.notifyAll();
  45. }
  46. }
  47. }
  48. }

当我们在不加锁时候,使用接口测试工具测试时,12345并不能都是顺序执行的,会造成输出顺序不一致,如果是在我们的实际场景中,这是输入换成了数据库的select和update,数据出现错乱也是很正常的情况了。

当我们加上锁以后,12345都是顺序输出,并发问题顺利解决了。

四、附录

1、Redis存在的bug

本来lock()方法是直接调用 "Redis.setIfAbsent()" 方法,但是在使用时候一直报空指针异常,最终定位问题为Redis.setIfAbsent()方法存在问题。

在我的实际业务中,下订单的方法使用了@Transflastion增加了事务,导致该方法返回null,我们手写一个实现setIfAbsent()的作用。

  1. /**
  2. * 只有key不存在时,才设置值, 返回true, 否则返回false
  3. *
  4. * @param key key 不能为null
  5. * @param value value 不能为null
  6. * @param timeout 过期时长, 单位为妙
  7. * @return
  8. */
  9. public static Boolean setLock(String key,String value, long timeout) {
  10. SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
  11. List<Object> exec = null;
  12. @Override
  13. @SuppressWarnings("unchecked")
  14. public Boolean execute(RedisOperations operations) throws DataAccessException {
  15. operations.multi();
  16. redisTemplate.opsForValue().setIfAbsent(key, value);
  17. redisTemplate.expire(key,timeout, TimeUnit.SECONDS);
  18. exec = operations.exec();
  19. if(exec.size() > 0) {
  20. return (Boolean) exec.get(0);
  21. }
  22. return false;
  23. }
  24. };
  25. return (Boolean) redisTemplate.execute(sessionCallback);
  26. }

方便对比,以下贴上原本的setIfAbsent()方法。

  1. /**
  2. * 只有key不存在时,才设置值, 返回true, 否则返回false [警告:事务或者管道情况下会报错-可使用 setLock方法]
  3. *
  4. * @param key key 不能为null
  5. * @param value value 不能为null
  6. * @param timeout 过期时长, 单位为妙
  7. * @return
  8. */
  9. @Deprecated
  10. public static <T> Boolean setIfAbsent(String key, T value, long timeout) {
  11. // redisTemplate.multi();
  12. ValueOperations<String, T> valueOperations = redisTemplate.opsForValue();
  13. Boolean aBoolean = valueOperations.setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
  14. // redisTemplate.exec();
  15. return aBoolean;
  16. }

2、MySQL的锁机制

在并发场景下MySQL会报错,报错信息如下:

  1. ### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
  2. ; SQL []; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction

问题出现的原因是,某一种表频繁被锁表,导致另外一个事务超时,出现问题的原因是MySQL的机制。

MySQL更新时如果where字段存在索引会使用行锁,否则会使用表锁。

我们使用navichat在where字段上加上索引,问题顺利的迎刃而解。

Redis实现并发阻塞锁方案的更多相关文章

  1. Redis高并发分布式锁详解

    为什么需要分布式锁 1.为了解决Java共享内存模型带来的线程安全问题,我们可以通过加锁来保证资源访问的单一,如JVM内置锁synchronized,类级别的锁ReentrantLock. 2.但是随 ...

  2. Redis的并发竞争问题,你用哪些方案来解决?

    Redis的并发竞争问题,主要是发生在并发写竞争. 考虑到redis没有像db中的sql语句,update val = val + 10 where ...,无法使用这种方式进行对数据的更新. 假如有 ...

  3. 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存

    原文:http://blog.csdn.net/heyewu4107/article/details/71009712 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存 问 ...

  4. 12.redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗?

    作者:中华石杉 面试题 redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗? 面试官心理分析 这个也是线上非常常见的一个问题,就是多客户端同时并发写一个 ke ...

  5. 利用redis实现分布式事务锁,解决高并发环境下库存扣减

    利用redis实现分布式事务锁,解决高并发环境下库存扣减   问题描述: 某电商平台,首发一款新品手机,每人限购2台,预计会有10W的并发,在该情况下,如果扣减库存,保证不会超卖 解决方案一 利用数据 ...

  6. Redis实现高并发分布式锁

    分布式锁场景在分布式环境下多个操作需要以原子的方式执行首先启一个springboot项目,再引入redis依赖包: <!-- https://mvnrepository.com/artifa . ...

  7. Redis的并发竞争问题的解决方案总结

    什么是Redis的并发竞争问题 Redis的并发竞争问题,主要是发生在并发写竞争. 考虑到redis没有像db中的sql语句,update val = val + 10 where ...,无法使用这 ...

  8. Redis高并发和快速的原因

    一.Redis的高并发和快速原因 1.redis是基于内存的,内存的读写速度非常快: 2.redis是单线程的,省去了很多上下文切换线程的时间:   3.redis使用多路复用技术,可以处理并发的连接 ...

  9. redis客户端、分布式锁及数据一致性

    Redis Java客户端有很多的开源产品比如Redission.Jedis.lettuce等. Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持:Redis ...

随机推荐

  1. The Http request is not acceptable for the requested resource.

    一.问题来源 最近给第三方做了一个我们系统的免密登陆,开发完成本地测试没有问题,但是第三方调用免密登陆接口并跳转之后报如下错误: The Http request is not acceptable ...

  2. phptorm 集成git和gitlab和一些命令

    mac: 2.phpstorm->csv->create git... 3.命令:ssh-keygen -t rsa -C"your_email@youremail.com&qu ...

  3. Mybaits 的优点?

    1.基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任 何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理:提供 XML 标签,支持编写动态 SQL ...

  4. 面试问题之C++语言:Overload、Override及Overwirte的区别

    Overload(重载):在C++程序中,可以将语义.功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型.顺序不同),即函数重载. 特点: (1)相同的范围(在同一个类中) (2)函数名 ...

  5. jmeter的安装使用

    以前没自己做过压力测试,一直都是测试在做.现在需要自己做压力测试了,特别学习下jmeter的使用方法.现在做下记录: 1.下载jmeter,这个忽略,百度到处都是 2.打开jmeter,jmeter的 ...

  6. 学习saltstack (一)

    salt介绍 Salt是一个基础平台管理工具 Salt是一个配置管理系统,能够维护预定义状态的远程节点 Salt是一个分布式远程执行系统,用来在远程节点上执行命令和查询数据 salt的核心功能 是命令 ...

  7. Pandas数据统计函数

    Pandas数据统计函数 汇总类统计 唯一去重和按值计数 相关系数和协方差 0.读取csv数据 1.汇总类统计 2.唯一去重和按值计数 2.1 唯一性去重 一般不用于数值列,而是枚举.分类列 2.2 ...

  8. 汽车中的V流程开发

    各步骤的简介各步骤的简介 (1)Control Design and offline Simulation:算法模型构建和离线仿真(基于模型的设计).算法工程师用Matlab模型实现算法:并实施离线仿 ...

  9. window onerror 各浏览器下表现总结

    window onerror 各浏览器下表现总结 做前端错误上报,必然离不开window onerror,但window onerror在不同设备上表现并不一致,浏览器为避免信息泄露,在一些情况下并不 ...

  10. 用Java编写的猜拳小游戏

    学习目标: 熟练掌握各种循环语句 例题: 代码如下: // 综合案例分析,猜拳案例 // isContinue为是否开始游戏时你所输入的值 char isContinue; //y为开始,n为借宿 S ...