zoukankan      html  css  js  c++  java
  • Springboot分别使用乐观锁和分布式锁(基于redisson)完成高并发防超卖

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

    乐观锁

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

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

    @Version
    private Long version;

    @Transactional
        public synchronized void mult(Long goodsId) {
            PtGoods ptGoods = ptGoodsManager.find(goodsId);
            logger.info("----amount:" + ptGoods.getAmount());
     
            ptGoods.setAmount(ptGoods.getAmount() + 1);
            ptGoodsManager.update(ptGoods);
     
        }

    测试一下:

    for (int i = 0; i < 100; i++) {
                new Thread(() -> {
                    goodsService.mult(1L);
                }
                ).start();
     
            }

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

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

    redission方式:

    pom里加入依赖

    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson-spring-boot-starter</artifactId>
                <version>3.10.6</version>
            </dependency>

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

    spring:  
        redis:
            host: ${REDIS_HOST:127.0.0.1}
            port: ${REDIS_PORT:6379}
            password: ${REDIS_PASSWORD:}

    配置redisson的客户端bean

    @Configuration
    public class RedisConfig {
        @Value("${spring.redis.host}")
        private String host;
     
        @Bean(name = {"redisTemplate", "stringRedisTemplate"})
        public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
            StringRedisTemplate redisTemplate = new StringRedisTemplate();
            redisTemplate.setConnectionFactory(factory);
            return redisTemplate;
        }
     
        @Bean
        public Redisson redisson() {
            Config config = new Config();
            config.useSingleServer().setAddress("redis://" + host + ":6379");
            return (Redisson) Redisson.create(config);
        }
     
    }

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

    我们创建一个redis锁的注解

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
     
    /**
     * @author wuweifeng wrote on 2019/5/8.
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RedissonLock {
        /**
         * 要锁哪个参数
         */
        int lockIndex() default -1;
     
        /**
         * 锁多久后自动释放(单位:秒)
         */
        int leaseTime() default 10;
    }

    切面类:

    import com.tianyalei.giftmall.global.annotation.RedissonLock;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.redisson.Redisson;
    import org.redisson.api.RLock;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
     
    import javax.annotation.Resource;
    import java.util.concurrent.TimeUnit;
     
    /**
     * 分布式锁
     * @author wuweifeng wrote on 2019/5/8.
     */
    @Aspect
    @Component
    @Order(1) //该order必须设置,很关键
    public class RedissonLockAspect {
        private Logger log = LoggerFactory.getLogger(getClass());
        @Resource
        private Redisson redisson;
     
        @Around("@annotation(redissonLock)")
        public Object around(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws Throwable {
            Object obj = null;
     
            //方法内的所有参数
            Object[] params = joinPoint.getArgs();
     
            int lockIndex = redissonLock.lockIndex();
            //取得方法名
            String key = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint
                    .getSignature().getName();
            //-1代表锁整个方法,而非具体锁哪条数据
            if (lockIndex != -1) {
                key += params[lockIndex];
            }
     
            //多久会自动释放,默认10秒
            int leaseTime = redissonLock.leaseTime();
            int waitTime = 5;
     
            RLock rLock = redisson.getLock(key);
            boolean res = rLock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
            if (res) {
                log.info("取到锁");
                obj = joinPoint.proceed();
                rLock.unlock();
                log.info("释放锁");
            } else {
                log.info("----------nono----------");
                throw new RuntimeException("没有获得锁");
            }
     
            return obj;
        }
    }

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

     

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

    测试类:

    package com.tianyalei.giftmall;
     
    import com.tianyalei.giftmall.core.goods.GoodsService;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
     
    import javax.annotation.Resource;
    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
     
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class GiftmallApplicationTests {
        @Resource
        private GoodsService goodsService;
     
        private CyclicBarrier cyclicBarrier = new CyclicBarrier(100);
        private CyclicBarrier cyclicBarrier1 = new CyclicBarrier(100);
     
        @Test
        public void contextLoads() {
            for (int i = 0; i < 100; i++) {
                new Thread(() -> {
                    try {
                        cyclicBarrier.await();
     
                        goodsService.multi(1L);
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
                ).start();
                new Thread(() -> {
                    try {
                        cyclicBarrier1.await();
     
                        goodsService.multi(2L);
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
                ).start();
            }
     
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
     
    }

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

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

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

  • 相关阅读:
    Spring-web初始化流程简图
    记一次升级Tomcat
    Spring-Task思维导图
    2019的第一个工作日
    RocketMQ专题2:三种常用生产消费方式(顺序、广播、定时)以及顺序消费源码探究
    RocketMQ专题1:入门
    博客搬家到云栖社区
    ActiveMQ专题2: 持久化
    ActiveMQ专题1: 入门实例
    linux下怎么卸载自带的JDK和安装想要的JDK
  • 原文地址:https://www.cnblogs.com/shihaiming/p/11082398.html
Copyright © 2011-2022 走看看