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