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来做数量的增减,能迅速的给出客户端是否抢到了商品的判断,之后再通过消息队列去生成订单之类的耗时操作。
看完打开支付宝扫一扫领个红包吧!