原文 :https://blog.csdn.net/tianyaleixiaowu/article/details/90036180

乐观锁

乐观锁就是在修改时,带上version版本号。这样如果试图修改已被别人修改过的数据时,会抛出异常。在一定程度上,也可以作为防超卖的一种处理方法。我们来看一下。

我们在Goods的entity类上,加上这个字段。

@Version
private Long version;

  1. @Transactional
  2. public synchronized void mult(Long goodsId) {
  3. PtGoods ptGoods = ptGoodsManager.find(goodsId);
  4. logger.info("----amount:" + ptGoods.getAmount());
  5.  
  6. ptGoods.setAmount(ptGoods.getAmount() + 1);
  7. ptGoodsManager.update(ptGoods);
  8.  
  9. }

测试一下:

  1. for (int i = 0; i < 100; i++) {
  2. new Thread(() -> {
  3. goodsService.mult(1L);
  4. }
  5. ).start();
  6.  
  7. }

可以发现,抛出了很多异常,这就是乐观锁的异常。可想而知,当高并发购买同一个商品时,会出现大量的购买失败,而不会出现超卖的情况,因为他限制了并发的访问修改。

这样其实显而易见,也是大有问题的,只适应于读多写少的情况,否则大量的失败也是有损用户体验,明明有货,却不卖出。

redission方式:

pom里加入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.redisson</groupId>
  7. <artifactId>redisson-spring-boot-starter</artifactId>
  8. <version>3.10.6</version>
  9. </dependency>

redisson支持单点、集群等模式,这里选择单点的。application.yml配置好redis的连接:

  1. spring:
  2. redis:
  3. host: ${REDIS_HOST:127.0.0.1}
  4. port: ${REDIS_PORT:6379}
  5. password: ${REDIS_PASSWORD:}

配置redisson的客户端bean

  1. @Configuration
  2. public class RedisConfig {
  3. @Value("${spring.redis.host}")
  4. private String host;
  5.  
  6. @Bean(name = {"redisTemplate", "stringRedisTemplate"})
  7. public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
  8. StringRedisTemplate redisTemplate = new StringRedisTemplate();
  9. redisTemplate.setConnectionFactory(factory);
  10. return redisTemplate;
  11. }
  12.  
  13. @Bean
  14. public Redisson redisson() {
  15. Config config = new Config();
  16. config.useSingleServer().setAddress("redis://" + host + ":6379");
  17. return (Redisson) Redisson.create(config);
  18. }
  19.  
  20. }

至于使用redisson的功能也很少,其实就是对并发访问的方法加个锁即可,方法执行完后释放锁。这样下一个请求才能进入到该方法。

我们创建一个redis锁的注解

  1. import java.lang.annotation.ElementType;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. import java.lang.annotation.Target;
  5.  
  6. /**
  7. * @author wuweifeng wrote on 2019/5/8.
  8. */
  9. @Target(ElementType.METHOD)
  10. @Retention(RetentionPolicy.RUNTIME)
  11. public @interface RedissonLock {
  12. /**
  13. * 要锁哪个参数
  14. */
  15. int lockIndex() default -1;
  16.  
  17. /**
  18. * 锁多久后自动释放(单位:秒)
  19. */
  20. int leaseTime() default 10;
  21. }

切面类:

  1. import com.tianyalei.giftmall.global.annotation.RedissonLock;
  2. import org.aspectj.lang.ProceedingJoinPoint;
  3. import org.aspectj.lang.annotation.Around;
  4. import org.aspectj.lang.annotation.Aspect;
  5. import org.redisson.Redisson;
  6. import org.redisson.api.RLock;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import org.springframework.core.annotation.Order;
  10. import org.springframework.stereotype.Component;
  11.  
  12. import javax.annotation.Resource;
  13. import java.util.concurrent.TimeUnit;
  14.  
  15. /**
  16. * 分布式锁
  17. * @author wuweifeng wrote on 2019/5/8.
  18. */
  19. @Aspect
  20. @Component
  21. @Order(1) //该order必须设置,很关键
  22. public class RedissonLockAspect {
  23. private Logger log = LoggerFactory.getLogger(getClass());
  24. @Resource
  25. private Redisson redisson;
  26.  
  27. @Around("@annotation(redissonLock)")
  28. public Object around(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws Throwable {
  29. Object obj = null;
  30.  
  31. //方法内的所有参数
  32. Object[] params = joinPoint.getArgs();
  33.  
  34. int lockIndex = redissonLock.lockIndex();
  35. //取得方法名
  36. String key = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint
  37. .getSignature().getName();
  38. //-1代表锁整个方法,而非具体锁哪条数据
  39. if (lockIndex != -1) {
  40. key += params[lockIndex];
  41. }
  42.  
  43. //多久会自动释放,默认10秒
  44. int leaseTime = redissonLock.leaseTime();
  45. int waitTime = 5;
  46.  
  47. RLock rLock = redisson.getLock(key);
  48. boolean res = rLock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
  49. if (res) {
  50. log.info("取到锁");
  51. obj = joinPoint.proceed();
  52. rLock.unlock();
  53. log.info("释放锁");
  54. } else {
  55. log.info("----------nono----------");
  56. throw new RuntimeException("没有获得锁");
  57. }
  58.  
  59. return obj;
  60. }
  61. }

这里解释一下,防超卖,其实是对某一个商品在被修改时进行加锁,而这个时候其他的商品是不受影响的。所以不能去锁整个方法,而应该是锁某个商品。所以我设置了一个lockIndex的参数,来指明你要锁的是方法的哪个属性,这里就是锁goodsId,如果不写,则是锁整个方法。

在切面里里面RLock.tryLock,则是最多等待5秒,托若还没取到锁就走失败,取到了则进入方法走逻辑。第二个参数是自动释放锁的时间,以避免自己刚取到锁,就挂掉了,导致锁无法释放。

测试类:

  1. package com.tianyalei.giftmall;
  2.  
  3. import com.tianyalei.giftmall.core.goods.GoodsService;
  4. import org.junit.Test;
  5. import org.junit.runner.RunWith;
  6. import org.springframework.boot.test.context.SpringBootTest;
  7. import org.springframework.test.context.junit4.SpringRunner;
  8.  
  9. import javax.annotation.Resource;
  10. import java.util.concurrent.BrokenBarrierException;
  11. import java.util.concurrent.CyclicBarrier;
  12.  
  13. @RunWith(SpringRunner.class)
  14. @SpringBootTest
  15. public class GiftmallApplicationTests {
  16. @Resource
  17. private GoodsService goodsService;
  18.  
  19. private CyclicBarrier cyclicBarrier = new CyclicBarrier(100);
  20. private CyclicBarrier cyclicBarrier1 = new CyclicBarrier(100);
  21.  
  22. @Test
  23. public void contextLoads() {
  24. for (int i = 0; i < 100; i++) {
  25. new Thread(() -> {
  26. try {
  27. cyclicBarrier.await();
  28.  
  29. goodsService.multi(1L);
  30. } catch (InterruptedException | BrokenBarrierException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. ).start();
  35. new Thread(() -> {
  36. try {
  37. cyclicBarrier1.await();
  38.  
  39. goodsService.multi(2L);
  40. } catch (InterruptedException | BrokenBarrierException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. ).start();
  45. }
  46.  
  47. try {
  48. Thread.sleep(6000);
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. }
  53.  
  54. }

这里用100并发,同时操作2个商品。

可以看到,这两个商品在各自更新各自的,互不影响。最终在5秒后,有的超时了。调大等待时间,则能保证每个都是100.

通过这种方式,即完成了分布式锁,简单也便捷。当然这里只是举例,在实际项目中,倘若要做防止超卖,以追求最大性能的话,也可以考虑使用redis来存储amount,借助于redis的increase来做数量的增减,能迅速的给出客户端是否抢到了商品的判断,之后再通过消息队列去生成订单之类的耗时操作。

Springboot分别使用乐观锁和分布式锁(基于redisson)完成高并发防超卖的更多相关文章

  1. MasaFramework -- 锁与分布式锁

    前言 什么是锁?什么是分布式锁?它们之间有什么样的关系? 什么是锁 加锁(lock)是2018年公布的计算机科学技术名词,是指将控制变量置位,控制共享资源不能被其他线程访问.通过加锁,可以确保在同一时 ...

  2. Java锁?分布式锁?乐观锁?行锁?

    转载自:公众号来源:码农翻身 作者:刘欣 Tomcat的锁 Tomcat是这个系统的核心组成部分, 每当有用户请求过来,Tomcat就会从线程池里找个线程来处理,有的执行登录,有的查看购物车,有的下订 ...

  3. 利用MySQL中的乐观锁和悲观锁实现分布式锁

    背景 对于一些并发量不是很高的场景,使用MySQL的乐观锁实现会比较精简且巧妙. 下面就一个小例子,针对不加锁.乐观锁以及悲观锁这三种方式来实现. 主要是一个用户表,它有一个年龄的字段,然后并发地对其 ...

  4. 锁、分布式锁、无锁实战全局性ID

    1.为什么要使用锁 当发生并发时,会产生多线程争夺一个资源,为保证资源的唯一性. JVM锁:对象锁,死锁,重入锁,公平锁,偏向锁 分布式锁:数据库 nosql .zookeeper 面试题:如何排查死 ...

  5. redis 加锁与释放锁(分布式锁1)

    使用Redis的 SETNX 命令可以实现分布式锁 SETNX key value 返回值 返回整数,具体为 - 1,当 key 的值被设置 - 0,当 key 的值没被设置 分布式锁使用 impor ...

  6. redis 加锁与释放锁(分布式锁)

    使用Redis的 SETNX 命令可以实现分布式锁 SETNX key value 返回值 返回整数,具体为 - 1,当 key 的值被设置 - 0,当 key 的值没被设置

  7. django celery的分布式异步之路(二) 高并发

    当你跑通了前面一个demo,博客地址:http://www.cnblogs.com/kangoroo/p/7299920.html,那么你的分布式异步之旅已经起步了. 性能和稳定性是web服务的核心评 ...

  8. SpringBoot项目框架下ThreadPoolExecutor线程池+Queue缓冲队列实现高并发中进行下单业务

    主要是自己在项目中(中小型项目) 有支付下单业务(只是办理VIP,没有涉及到商品库存),目前用户量还没有上来,目前没有出现问题,但是想到如果用户量变大,下单并发量变大,可能会出现一系列的问题,趁着空闲 ...

  9. springboot(十二)-分布式锁(redis)

    什么是分布式锁? 要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁.进程锁. 线程锁:主要用来给方法.代码块加锁.当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段.线程锁只在同一 ...

随机推荐

  1. Elasticsearch - 处理冲突

    http://blog.csdn.net/xifeijian/article/details/49615559

  2. MySQL普通索引性能试验

    首先使用如下node.js脚本创建两张表,并为这两张表各自生成10000条数据: var fs = require('fs'); var nameS = "赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱 ...

  3. LODOP中table自动分页补线加border

    LODOP中可以用ADD_PRINT_TABLE.ADD_PRINT_HTM.ADD_PRINT_HTML.ADD_PRINT_TBURL等可以输出超文本的表格,超文有超过打印项高度或纸张高度自动分页 ...

  4. 原生JavaScript常用本地浏览器存储方法四(HTML5 LocalStorage sessionStorage)

    HTML5 LocalStorage浏览器的支持的情况如上图,IE在8.0的时候就支持了.不过需要注意的是,IE测试的时候需要服务器环境(或者localhost). 测试自然是检测浏览器是否支持本地存 ...

  5. ROW_NUMBER()函数使用详解

    原文地址:https://blog.csdn.net/qq_30908543/article/details/74108348 注:mysql貌似不适用,本人测试未成功,mysql实现方式可参考:ht ...

  6. 【神经网络与深度学习】【计算机视觉】RCNN- 将CNN引入目标检测的开山之作

    转自:https://zhuanlan.zhihu.com/p/23006190?refer=xiaoleimlnote 前面一直在写传统机器学习.从本篇开始写一写 深度学习的内容. 可能需要一定的神 ...

  7. 【OpenGL开发】关于GLEW扩展库

    GLEW是一个跨平台的C++扩展库,基于OpenGL图形接口.使用OpenGL的朋友都知道,window目前只支持OpenGL1.1的涵数,但 OpenGL现在都发展到2.0以上了,要使用这些Open ...

  8. eNSP基于接口地址池的dhcp服务

    拓扑图如下 基于接口的dhcp是最简单的一种 我们对路由器的两个端口分别设置ip地址为192.168.1.254 192.168.2.254 然后分别进入接口进行下一步配置 dhcp select i ...

  9. linux 文件描述符表 打开文件表 inode vnode

      在Linux中,进程是通过文件描述符(file descriptors,简称fd)而不是文件名来访问文件的,文件描述符实际上是一个整数.Linux中规定每个进程能最多能同时使用NR_OPEN个文件 ...

  10. mysql 排除系统库的全部备份

    前言: 有些时候,我们要对数据库进行备份的时候,由于GTID的缘故,导出系统库后,再次导入其他环境的数据库时,就会出问题.所以,我们需要排掉一些系统库,排除GTID对于数据库迁移的影响.   方法: ...