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來做數量的增減,能迅速的給出客戶端是否搶到了商品的判斷,之后再通過消息隊列去生成訂單之類的耗時操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM