【 Redis | 實戰篇 秒殺實現 】

目錄

前言:

1.全局ID生成器

2.秒殺優惠券

2.1.秒殺優惠券的基本實現

2.2.超賣問題

2.3.解決超賣問題的方案

2.4.基于樂觀鎖來解決超賣問題

3.秒殺一人一單

3.1.秒殺一人一單的基本實現

3.2.單機模式下的線程安全問題

3.3.集群模式下的線程安全問題


前言:

實現全局ID生成器,秒殺優惠券(基于樂觀鎖解決超賣問題),秒殺的一人一單(單機與集群線程安全問題)

1.全局ID生成器

1.1.思考:由于之前一直都在數據庫中設置id為自增長字段(自增1),以訂單id舉例,那么會出現什么問題呢?

  • 訂單id每次新增一個訂單就自增1,那么這樣id規律性太明顯了,用戶可以直接根據它每次下的訂單id來判斷商家的營收情況(獲取到了商家的數據)
  • 如果訂單量多,數據庫一張表已經無法滿足保存這么多數據了,我們需要分表來保存數據,但是由于我們設置的id自增(每張表都是從1開始自增),因此我們的訂單id將會重復,在以后售后處理時,我們需要根據訂單id來查詢訂單信息,而訂單id有重復的,那么就不便于我們進行售后處理

1.2.訂單id的特性:

  • 訂單量大
  • id要唯一

1.3.全局ID生成器的要求:

  • 唯一性:保證id唯一
  • 高可用性:保證無論什么時候使用都可以生成正確的id
  • 高性能性:保證生產的id的速度足夠快
  • 遞增性:保證id的生產一定是整體逐漸遞增的,有利于數據庫創建索引增加插入速度
  • 安全性:規律性不能太明顯

1.4.實現方案:

  1. UUID:生成16進制最終轉換成字符串(無序并且不自增)
  2. Redis自增::第1位是符號位,始終為0;接下來的31位是時間戳,記錄了ID生成的時間;最后的32位是序列號,生成64位的二進制最終形成long類型數據
  3. snowflake(雪花算法):第1位是符號位,始終為0;接下來的41位是時間戳,記錄了ID生成的時間;然后的10位是工作進程ID,用于區分不同的服務器或進程;最后的12位是序列號,用于在同一毫秒內生成不同的ID,生成64位的二進制最終形成long類型數據
  4. 數據庫自增:單獨使用一張表來存生成的id值,其他要使用id的表就來查詢即可

1.5.具體實現(Redis自增方案):

為什么可以實現:

  • 唯一:由于Redis是獨立于數據庫之外的(不管有幾張表或者是有幾個數據庫),我們的Redis始終是只有一個(唯一),因此它的自增的id就永遠唯一
  • 高可用:利用集群,哨兵,主從方案
  • 高性能:Redis基于內存,數據庫基于硬盤,因此性能更好
  • 遞增:Redis自帶命令可以實現自增
  • 安全性:不會直接使用Redis的自增數值(依舊是規律性太明顯),采用拼接信息實現

怎么實現:我們采用拼接信息實現,而為了增加性能,我們采用數值類型(long類型),它占用空間小,對建立索引方便

實現步驟:拼接信息,第1位是符號位,始終為0(0位正,1為負);接下來的31位是時間戳(秒數),記錄了ID生成的時間;最后的32位是序列號(Redis自增數),生成64位的二進制最終形成long類型數據

解釋:

時間戳(秒數):利用當前時間減去你自己設置的開始時間最后得到的時間秒數

------------

思考:那為什么不直接使用當前時間的秒數呢

解釋:還是由于使用當前時間秒數容易被猜到規律,規律性明顯

序列號:Redis自增數

------------

實現:Redis自增數使用String類型中的命令increment(每次自增1),并且由于該命令是如果Redis中沒有key就會幫你自動創建key然后自增(此時值為1),存在key那么就直接將key中的value自增1,最終返回value值

------------

細節:由于使用的Redis的命令那么最終序列號作為value將存入Redis,那么存入Redis的自增數不就是我們的訂單數嗎?那以后我們需要統計訂單數是不是直接查詢Redis就行,而為了方便查詢,我們的key是不是需要設置一個有意義的(通過key)

-------------

key的設置:自己設置前綴(以后生成id的不只是訂單id,因此我們需要自己指定對應前綴來區分),然后用前綴拼接時間(具體到天),最終形成一個key

------------

思考:加前綴我能理解,為了區分存入Redis的key,那為什么還要拼接時間呢?

解釋:如果你的序列號都使用同一個key,Redis存入是由上限的,而且為了你以后方便查詢,key拼接時間(具體到天),那么我們可以統計每一天的下單量

實現細節:

思考:我們最終得到了時間戳(秒)long類型,序列號(訂單數)long類型,我們需要拼接形成一個全新的long,符號位不需要管(正數0,負數1)

步驟:將時間戳向左移32位(留給序列號的),由于向左移位時以0來填充,那么再將移位后的時間戳異或上序列號即可(只有有一個為真那就是真,有1就是1),第一位符號位不需要管,時間戳是正數(id一般也會設置為正數),最終形成一個新的long類型的id

解釋:我們這里是進行的二進制計算,而二進制只有0/1,那么有值就為1,沒有值就為0了(異或)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;@Component
public class RedisIdWorker {@Autowiredprivate final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//定義開始時間戳private static final Long BEGIN_TIME_SECOND = 1740960000L;//移動位數private static final Long COUNT_BIT = 32L;public Long setId(String keyPrefix){//1.設置時間戳//當前時間戳LocalDateTime now = LocalDateTime.now();long second = now.toEpochSecond(ZoneOffset.UTC);//最終時間戳Long time = second - BEGIN_TIME_SECOND;//2.獲取序列號String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//Redis返回的序列號long increment = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + data);//拼接return time << COUNT_BIT | increment;}
}

Redis圖效果:?

2.秒殺優惠券

2.1.秒殺優惠券的基本實現

思考:在下單優惠券之前,我們需要判斷兩點

  • 秒殺是否開始或結束
  • 庫存是否充足

步驟:

前端提交優惠券id

==》后端接收id

==》根據優惠券id查詢數據庫,得到優惠券信息

==》判斷秒殺是否開始或結束

==》秒殺沒有開始或已經結束

==》返回錯誤信息

-------

==》秒殺正在進行

==》判斷庫存是否充足

==》不足

==》返回錯誤信息

-------

==》充足

==》扣減庫存

==》創建訂單

==》返回訂單id

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//1.根據id查詢數據庫優惠券信息SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2.獲取時間LocalDateTime beginTime = voucher.getBeginTime();LocalDateTime endTime = voucher.getEndTime();//3.判斷時間if (beginTime.isAfter(LocalDateTime.now())) {return Result.fail("秒殺還未開始");}if (endTime.isBefore(LocalDateTime.now())) {return Result.fail("秒殺已經結束");}//4.獲取庫存Integer stock = voucher.getStock();//庫存不足if(stock < 1){return Result.fail("庫存不足");}//庫存足//5.庫存減1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).update();if (!success){return Result.fail("庫存不足");}//6.創建訂單VoucherOrder voucherOrder = new VoucherOrder();//優惠券idvoucherOrder.setVoucherId(voucherId);//訂單idLong orderId = redisIdWorker.setId("order");voucherOrder.setId(orderId);//用戶idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);//存入數據庫save(voucherOrder);return Result.ok(orderId);}}

@Component
public class RedisIdWorker {@Autowiredprivate final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//定義開始時間戳private static final Long BEGIN_TIME_SECOND = 1740960000L;//移動位數private static final Long COUNT_BIT = 32L;public Long setId(String keyPrefix){//1.設置時間戳//當前時間戳LocalDateTime now = LocalDateTime.now();long second = now.toEpochSecond(ZoneOffset.UTC);//最終時間戳Long time = second - BEGIN_TIME_SECOND;//2.獲取序列號String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//Redis返回的序列號long increment = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + data);//拼接return time << COUNT_BIT | increment;}
}

?解釋:我們唯一要注意的點就是秒殺的時間和庫存的數量(判斷)

2.2.超賣問題

解釋:

  • 前提:庫存此時為1

例子:線程1先執行查詢庫存,線程2再執行查詢,線程1扣減庫存,線程2扣減庫存

==》線程1先執行

==》線程1查詢庫存(1)

==》線程2搶到執行權

==》線程2查詢庫存(1)

==》線程1再次搶到執行權

==》由于庫存大于0

==》線程1執行庫存扣減操作

==》此時庫存(0)

==》線程2執行

==》由于之前查詢庫存結果為1

==》線程2也執行庫存扣減操作

==》此時庫存(-1)

----------

那么此時優惠券庫存為-1,已經形成了超賣問題

2.3.解決超賣問題的方案

解決方案:

方案一:悲觀鎖

悲觀鎖:認為線程安全問題一定會發生,因此在每次操作數據之前先獲取鎖,以此確保線程安全,保證線程串行執行

  • Synchronized,Lock都屬于悲觀鎖
  • 優點:簡單粗暴
  • 缺點:性能一般

方案二:樂觀鎖

樂觀鎖:認為線程安全問題不一定發生,因此不加鎖,只是在更新數據時去判斷有沒有其他線程來對數據進行了修改

  • 如果沒有修改則認為是安全的,自己才更新數據
  • 如果已經被其他線程修改說明發生了安全問題,此時可以重試或返回異常
  • 優點:性能好
  • 缺點:存在安全率低的問題

解釋:悲觀鎖就是直接加鎖,由于是加鎖其他線程都需要等待因此性能低,樂觀鎖是不加鎖,由于不加鎖那么就會出現安全問題(概率低)

思考:

  1. 由于我們是優惠券庫存問題(有數據給我們判斷,這個數據到底有沒有修改過),我們可以直接根據庫存來判斷是否出現數據不一致問題,那么就可以采用樂觀鎖
  2. 如果不是庫存呢,那么只能通過數據的整體變化來判斷,此時采用樂觀鎖是復雜的,你需要判斷的數據太多了,那么就采用悲觀鎖
  3. 但是悲觀鎖的性能一般,怎么提高性能呢:采用分批加鎖(分段鎖),將數據分成幾份(假設分成10張表),那么用戶是不是同時去這10張表搶,同時10個人搶(效率提高),最終思想:每次鎖定的資源少

總結:如果要更新數據那么可以使用樂觀鎖,添加數據使用悲觀鎖

2.4.基于樂觀鎖來解決超賣問題

版本號法:設置版本號,每次查詢庫存時也查詢版本號,最后扣減庫存時增加判斷條件(就是此時的版本號應該等于我先前查詢到的版本號),如果不等于事務回滾

思想:更新數據前比較版本號是否發生改變

步驟:

前端提交優惠券id

==》后端接收id

==》根據優惠券id查詢數據庫,得到優惠券信息,獲取版本號

==》判斷秒殺是否開始或結束

==》秒殺沒有開始或已經結束

==》返回錯誤信息

-------

==》秒殺正在進行

==》判斷庫存是否充足

==》不足

==》返回錯誤信息

-------

==》充足

==》判斷版本號是否發生改變

==》改變返回錯誤信息

-------

==》版本號相同

==》扣減庫存

==》創建訂單

==》返回訂單id

CAS法:直接比較庫存,在更新數據時增加判斷條件(庫存是否發生改變),庫存改變不執行更新操作事務回滾

思想:直接利用已有數據來進行判斷,根據數據是否發生變化來確定是否更新數據

步驟:

前端提交優惠券id

==》后端接收id

==》根據優惠券id查詢數據庫,得到優惠券信息

==》判斷秒殺是否開始或結束

==》秒殺沒有開始或已經結束

==》返回錯誤信息

-------

==》秒殺正在進行

==》判斷庫存是否充足

==》不足

==》返回錯誤信息

-------

==》充足

==》判斷庫存是否發生變化

==》改變返回錯誤信息

-------

==》庫存相同

==》扣減庫存

==》創建訂單

==》返回訂單id

思考:由于我們是優惠券庫存問題,那么我們可以直接使用庫存來直接判斷,只有庫存發生變化,那我們就不進行更新操作

代碼實現:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//1.根據id查詢數據庫優惠券信息SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2.獲取時間LocalDateTime beginTime = voucher.getBeginTime();LocalDateTime endTime = voucher.getEndTime();//3.判斷時間if (beginTime.isAfter(LocalDateTime.now())) {return Result.fail("秒殺還未開始");}if (endTime.isBefore(LocalDateTime.now())) {return Result.fail("秒殺已經結束");}//4.獲取庫存Integer stock = voucher.getStock();//庫存不足if(stock < 1){return Result.fail("庫存不足");}//庫存足//5.庫存減1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).eq("stock",stock)//樂觀鎖.update();if (!success){return Result.fail("庫存不足");}//6.創建訂單VoucherOrder voucherOrder = new VoucherOrder();//優惠券idvoucherOrder.setVoucherId(voucherId);//訂單idLong orderId = redisIdWorker.setId("order");voucherOrder.setId(orderId);//用戶idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);//存入數據庫save(voucherOrder);return Result.ok(orderId);}}

@Component
public class RedisIdWorker {@Autowiredprivate final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//定義開始時間戳private static final Long BEGIN_TIME_SECOND = 1740960000L;//移動位數private static final Long COUNT_BIT = 32L;public Long setId(String keyPrefix){//1.設置時間戳//當前時間戳LocalDateTime now = LocalDateTime.now();long second = now.toEpochSecond(ZoneOffset.UTC);//最終時間戳Long time = second - BEGIN_TIME_SECOND;//2.獲取序列號String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//Redis返回的序列號long increment = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + data);//拼接return time << COUNT_BIT | increment;}
}

弊端:如果同時大量用戶搶優惠券,而此時庫存還有100張,用戶們都先進行了查詢庫存的操作,但都沒有進行庫存扣減操作,等到第一個先搶到優惠券后,庫存改變,那么其他用戶全部會搶券失敗

前提:優惠券庫存100

例子:100個線程都先進行了查詢庫存操作,都還沒有執行到判斷庫存是否發生改變

==》線程1-100查詢庫存(100)

==》線程1優先于其他線程先執行完判斷庫存操作(100)

==》線程1扣減庫存(99)

==》不管之后是哪個線程來執行判斷庫存操作

==》庫存已經發生變化,搶券失敗

----------

那么此時100個用戶搶券,只搶券成功一人,但是我的優惠券庫存卻還有99張,失敗率極高

怎么提高用戶搶券的成功率呢

思考:由于庫存不能是負數,那么我們最后判斷的條件不再是庫存是否改變,而是庫存大于0就行,只要有庫存那么我就賣給用戶,即使出現大量用戶同時進行搶券的情況,我們也可以將券買給用戶(而不是只能賣給第一個用戶),并且當庫存只有一張時,由于我們是更新操作,數據庫只允許一個線程來執行更新操作,不允許多個線程同時執行更新庫存操作(最后一張券被大量用戶搶時,總會有一個用戶搶到,其他用戶則搶不到)

步驟:

前端提交優惠券id

==》后端接收id

==》根據優惠券id查詢數據庫,得到優惠券信息

==》判斷秒殺是否開始或結束

==》秒殺沒有開始或已經結束

==》返回錯誤信息

-------

==》秒殺正在進行

==》判斷庫存是否充足

==》不足

==》返回錯誤信息

-------

==》充足

==》再次判斷庫存是否大于0

==》庫存不足返回錯誤信息

-------

==》庫存足

==》扣減庫存

==》創建訂單

==》返回訂單id

?代碼實現:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//1.根據id查詢數據庫優惠券信息SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2.獲取時間LocalDateTime beginTime = voucher.getBeginTime();LocalDateTime endTime = voucher.getEndTime();//3.判斷時間if (beginTime.isAfter(LocalDateTime.now())) {return Result.fail("秒殺還未開始");}if (endTime.isBefore(LocalDateTime.now())) {return Result.fail("秒殺已經結束");}//4.獲取庫存Integer stock = voucher.getStock();//庫存不足if(stock < 1){return Result.fail("庫存不足");}//庫存足//5.庫存減1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId)..gt("stock",0)//樂觀鎖.update();if (!success){return Result.fail("庫存不足");}//6.創建訂單VoucherOrder voucherOrder = new VoucherOrder();//優惠券idvoucherOrder.setVoucherId(voucherId);//訂單idLong orderId = redisIdWorker.setId("order");voucherOrder.setId(orderId);//用戶idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);//存入數據庫save(voucherOrder);return Result.ok(orderId);}}

@Component
public class RedisIdWorker {@Autowiredprivate final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//定義開始時間戳private static final Long BEGIN_TIME_SECOND = 1740960000L;//移動位數private static final Long COUNT_BIT = 32L;public Long setId(String keyPrefix){//1.設置時間戳//當前時間戳LocalDateTime now = LocalDateTime.now();long second = now.toEpochSecond(ZoneOffset.UTC);//最終時間戳Long time = second - BEGIN_TIME_SECOND;//2.獲取序列號String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//Redis返回的序列號long increment = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + data);//拼接return time << COUNT_BIT | increment;}
}

3.秒殺一人一單

3.1.秒殺一人一單的基本實現

思考:由于是秒殺問題,因此不能讓用戶一個人全部買走(這不就是黃牛嗎),那么我們可以實現一個用戶只能下一單

步驟:

前端提交優惠券id

==》后端接收id

==》根據優惠券id查詢數據庫,得到優惠券信息

==》判斷秒殺是否開始或結束

==》秒殺沒有開始或已經結束

==》返回錯誤信息

-------

==》秒殺正在進行

==》判斷庫存是否充足

==》不足

==》返回錯誤信息

-------

==》充足

==》根據優惠券id和用戶id來查詢數據庫,返回查詢數量

==》判斷數量是否大于0

==》大于0,即用戶已經下過一單(每張優惠券id不同)

==》返回錯誤信息

-------

==》數量小于0,即用戶沒有下單

==》再次判斷庫存是否大于0

==》庫存不足返回錯誤信息

-------

==》庫存足

==》扣減庫存

==》創建訂單

==》返回訂單id

?代碼實現:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//1.根據id查詢數據庫優惠券信息SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2.獲取時間LocalDateTime beginTime = voucher.getBeginTime();LocalDateTime endTime = voucher.getEndTime();//3.判斷時間if (beginTime.isAfter(LocalDateTime.now())) {return Result.fail("秒殺還未開始");}if (endTime.isBefore(LocalDateTime.now())) {return Result.fail("秒殺已經結束");}//4.獲取庫存Integer stock = voucher.getStock();//庫存不足if(stock < 1){return Result.fail("庫存不足");}//根據用戶id和優惠券id查詢數據庫Long userId = UserHolder.getUser().getId();int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count > 0){//該用戶已經下過單了return Result.fail("一個用戶只能下一單");}//庫存足//5.庫存減1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId)..gt("stock",0)//樂觀鎖.update();if (!success){return Result.fail("庫存不足");}//6.創建訂單VoucherOrder voucherOrder = new VoucherOrder();//優惠券idvoucherOrder.setVoucherId(voucherId);//訂單idLong orderId = redisIdWorker.setId("order");voucherOrder.setId(orderId);//用戶idvoucherOrder.setUserId(userId);//存入數據庫save(voucherOrder);return Result.ok(orderId);}}

@Component
public class RedisIdWorker {@Autowiredprivate final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//定義開始時間戳private static final Long BEGIN_TIME_SECOND = 1740960000L;//移動位數private static final Long COUNT_BIT = 32L;public Long setId(String keyPrefix){//1.設置時間戳//當前時間戳LocalDateTime now = LocalDateTime.now();long second = now.toEpochSecond(ZoneOffset.UTC);//最終時間戳Long time = second - BEGIN_TIME_SECOND;//2.獲取序列號String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//Redis返回的序列號long increment = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + data);//拼接return time << COUNT_BIT | increment;}
}

3.2.單機模式下的線程安全問題

解釋:

前提:庫存充足,并且是同一個用戶下單,此時該用戶還沒有下單(訂單數量0)

--------

例子:一個用戶同時發出倆個請求(買相同的優惠券),線程1先查詢,線程2后查詢,線程1判斷用戶是否下過單,線程2判斷用戶是否下過單

==》線程1先執行

==》線程1查詢訂單數量(0)

==》線程1判斷訂單數量

==》訂單數量為0,可以下單

==》線程2搶到執行權

==》線程2執行查詢訂單數量(0)

==》訂單數量為0,也可以下單

==》線程1搶到執行權

==》由于訂單數量為0,線程1執行下單操作

==》線程2執行

==》由于訂單數量為0,線程2執行下單操作

---------

那么最終一個用戶下了兩單,出現了并發安全問題

思考:這是不是還是超賣問題,那么還是使用鎖來解決,而我們現在是執行創建訂單的操作,樂觀鎖是需要根據數據的變化來實現的,因此不能使用樂觀鎖(修改用樂觀,添加用悲觀)

思路:既然使用悲觀鎖,那么我們需要考慮在哪里加鎖合適,是整個方法都加上鎖嗎?不是吧,我們最終問題出現在哪,是并發查詢訂單數量那里而之前的查詢庫存操作(等等)是不需要加鎖的(加鎖是會導致我們的性能降低,因此我們需要考慮加鎖的合適位置既然是對于方法內部部分代碼進行加鎖,那么我們可以將要加鎖的代碼抽離出來,對于這個新方法進行加鎖,而我們這里使用synchronized

@Transactionalpublic synchronized Result creatOrder(Long voucherId) {//根據用戶id和優惠券id查詢數據庫Long userId = UserHolder.getUser().getId();int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count > 0){//該用戶已經下過單了return Result.fail("一個用戶只能下一單");}//庫存足//5.庫存減1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock",0)//樂觀鎖.update();if (!success){return Result.fail("庫存不足");}//6.創建訂單VoucherOrder voucherOrder = new VoucherOrder();//優惠券idvoucherOrder.setVoucherId(voucherId);//訂單idLong orderId = redisIdWorker.setId("order");voucherOrder.setId(orderId);//用戶id
//        Long userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);//存入數據庫save(voucherOrder);return Result.ok(orderId);}

細節:我們是直接將鎖synchronized加在整個新方法上嗎?(返回值類型之前),不是吧,這樣我們鎖住的是整個方法(synchronized鎖對象是該類的實例),那么不同用戶都使用同一把鎖(串行執行,效率極低),注意:需要加上事務注解

思考:為了將效率提高,那么我們需要將鎖的范圍縮小一個用戶一把鎖(不同的用戶不同的鎖),不建議將synchronized直接加在方法上

實現:那么我們可以將方法內的代碼抽離出來形成代碼塊,然后對代碼塊加鎖synchronized,而為了保證一個用戶一把鎖,那么我們對于synchronized的定義該怎么辦

一個用戶一把鎖的問題我們之前不是取出來了用戶的id嗎,直接用id來定義synchronized,不對,如果直接用用戶id這個變量來定義鎖,那么相同用戶發出多次請求,請求的鎖不同(每次用戶id的創建地址不同),那我們直接用用戶id里面的id值就行(id.toString()),同樣不對,toString()方法的底層依舊是new一個新的String類型,那么還是地址不同,鎖不同

問題解決:使用id.toString().intern(),intern()方法的原理是雖然你toString()方法會new一個新的String對象,但是我會先去字符串池里找,找不到對應的值我才會new,找到了我直接復用該String地址,從而保證了用戶id的值一樣鎖的定義也一樣

@Transactionalpublic  Result creatOrder(Long voucherId) {//根據用戶id和優惠券id查詢數據庫Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count > 0){//該用戶已經下過單了return Result.fail("一個用戶只能下一單");}//庫存足//5.庫存減1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock",0)//樂觀鎖.update();if (!success){return Result.fail("庫存不足");}//6.創建訂單VoucherOrder voucherOrder = new VoucherOrder();//優惠券idvoucherOrder.setVoucherId(voucherId);//訂單idLong orderId = redisIdWorker.setId("order");voucherOrder.setId(orderId);//用戶idvoucherOrder.setUserId(userId);//存入數據庫save(voucherOrder);return Result.ok(orderId);}}

代碼塊鎖事務管理問題:由于此時鎖是加在方法內部的,而我們的事務管理是由Spring來管理,要等到鎖釋放后,方法執行完,才能進行事務提交(更新庫存,創建訂單),而此時鎖優先于事務提交之前就已經釋放了,那么其他的線程就可以進行操作,依然會出現并發問題

解釋:

前提:同一個用戶發出兩個請求,并且此時用戶沒有下單(訂單數0)

==》線程1先執行

==》線程1查詢訂單數量(0)

==》線程1獲取鎖成功,執行鎖內代碼

==》線程1釋放鎖,但是事務還未提交

==》線程2查詢訂單數量(0)

==》線程2獲取鎖成功,執行鎖內代碼

==》線程2釋放鎖

==》線程1事務提交成功(訂單加1)

==》線程2事務提交成功(訂單加1)

------

此時同一個用戶下了倆單

解決:既然是鎖和事務執行順序問題,那么我們先讓事務先執行,鎖后釋放,而由于我們已經將要加鎖的代碼抽離出來形成一個新的方法,那么我們可以在調用該方法時給它加鎖,從而鎖住整個函數,保證數據已經更新

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {//1.根據id查詢數據庫優惠券信息SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2.獲取時間LocalDateTime beginTime = voucher.getBeginTime();LocalDateTime endTime = voucher.getEndTime();//3.判斷時間if (beginTime.isAfter(LocalDateTime.now())) {return Result.fail("秒殺還未開始");}if (endTime.isBefore(LocalDateTime.now())) {return Result.fail("秒殺已經結束");}//4.獲取庫存Integer stock = voucher.getStock();//庫存不足if(stock < 1){return Result.fail("庫存不足");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){return creatOrder(voucherId);}}@Transactionalpublic  Result creatOrder(Long voucherId) {//根據用戶id和優惠券id查詢數據庫Long userId = UserHolder.getUser().getId();int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count > 0){//該用戶已經下過單了return Result.fail("一個用戶只能下一單");}//庫存足//5.庫存減1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock",0)//樂觀鎖.update();if (!success){return Result.fail("庫存不足");}//6.創建訂單VoucherOrder voucherOrder = new VoucherOrder();//優惠券idvoucherOrder.setVoucherId(voucherId);//訂單idLong orderId = redisIdWorker.setId("order");voucherOrder.setId(orderId);//用戶idvoucherOrder.setUserId(userId);//存入數據庫save(voucherOrder);return Result.ok(orderId);}}

@Component
public class RedisIdWorker {@Autowiredprivate final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//定義開始時間戳private static final Long BEGIN_TIME_SECOND = 1740960000L;//移動位數private static final Long COUNT_BIT = 32L;public Long setId(String keyPrefix){//1.設置時間戳//當前時間戳LocalDateTime now = LocalDateTime.now();long second = now.toEpochSecond(ZoneOffset.UTC);//最終時間戳Long time = second - BEGIN_TIME_SECOND;//2.獲取序列號String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//Redis返回的序列號long increment = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + data);//拼接return time << COUNT_BIT | increment;}
}

思考:由于我們使用的是方法調用方法(鎖的),而在相同類里方法調用方法使用的是this關鍵字,this代表當前類的對象(不是Spring的代理對象),而我們的事務生效是因為Spring對當前類實現了動態代理,是拿到了它的動態代理對象進行的事務管理,而現在的this調用是非代理對象不擁有事務功能(Spring事務失效的可能性之一),因此事務管理將會失效

解決:既然是沒有代理對象來調用方法,那么我們就使用代理對象來調用方法

實現:

  • 添加依賴
  • 啟動類添加注解(暴露代理對象)
  • 使用AopContet.currentProxy();獲取當前對象的代理對象

代碼實現:?

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {//1.根據id查詢數據庫優惠券信息SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2.獲取時間LocalDateTime beginTime = voucher.getBeginTime();LocalDateTime endTime = voucher.getEndTime();//3.判斷時間if (beginTime.isAfter(LocalDateTime.now())) {return Result.fail("秒殺還未開始");}if (endTime.isBefore(LocalDateTime.now())) {return Result.fail("秒殺已經結束");}//4.獲取庫存Integer stock = voucher.getStock();//庫存不足if(stock < 1){return Result.fail("庫存不足");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){//獲取代理對象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.creatOrder(voucherId);}}@Transactionalpublic  Result creatOrder(Long voucherId) {//根據用戶id和優惠券id查詢數據庫Long userId = UserHolder.getUser().getId();int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count > 0){//該用戶已經下過單了return Result.fail("一個用戶只能下一單");}//庫存足//5.庫存減1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock",0)//樂觀鎖.update();if (!success){return Result.fail("庫存不足");}//6.創建訂單VoucherOrder voucherOrder = new VoucherOrder();//優惠券idvoucherOrder.setVoucherId(voucherId);//訂單idLong orderId = redisIdWorker.setId("order");voucherOrder.setId(orderId);//用戶idvoucherOrder.setUserId(userId);//存入數據庫save(voucherOrder);return Result.ok(orderId);}}

@Component
public class RedisIdWorker {@Autowiredprivate final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//定義開始時間戳private static final Long BEGIN_TIME_SECOND = 1740960000L;//移動位數private static final Long COUNT_BIT = 32L;public Long setId(String keyPrefix){//1.設置時間戳//當前時間戳LocalDateTime now = LocalDateTime.now();long second = now.toEpochSecond(ZoneOffset.UTC);//最終時間戳Long time = second - BEGIN_TIME_SECOND;//2.獲取序列號String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//Redis返回的序列號long increment = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + data);//拼接return time << COUNT_BIT | increment;}
}

3.3.集群模式下的線程安全問題

原因:在集群的情況下,同一個用戶的多次請求如果請求到不同的Tomcat,那么鎖也會不同,依然會出現超賣問題

思考:在集群情況下有多臺Tomcat那么就會有多臺jvm,而不同的jvm的鎖(維護了一個鎖的監視器對象)是不同的

解釋:由于我們的鎖是基于用戶id來實現的,id記錄在常量池中,id相同則代表是同一個鎖(同一個監視器),就是監視器里有值了(值就是id),無論有多少個線程,只要第一個線程獲取到鎖(該用戶id值被記錄在監視器中),其他線程來獲取鎖,而鎖發現監視器已經有值了,那么線程會獲取鎖失敗,所以我們是基于看監視器對象是否記錄值,而不同的Tomcat的監視器對象并不共享,因此同一個用戶可以在多個Tomcat中形成多個鎖

當我們集群時

==》有一個新的部署

==》就會有一個新的Tomcat

==》就會有一個新的jvm

==》就會有一個新的監視器對象(不同的jvm有不同的監視器)

==》因此當id相同時,Tomcat不同時,可以重復獲取鎖

==》假設有2個jvm

==》2個監視器

==》2個相同的id鎖

-----

那么還是會出現線程安全問題,依舊是一個用戶可以根據Tomcat的多少來下多少單

總結:在集群/分布式系統的情況下會有多個jvm存在,由于我們使用的是jvm自帶的鎖synchronized,而每個jvm都有自己的鎖監視器對象,所以每個鎖都可以有一個線程來獲取,出現并行運行,出現安全問題

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/905294.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/905294.shtml
英文地址,請注明出處:http://en.pswp.cn/news/905294.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

如何用URDF文件構建機械手模型并與MoveIt集成

機械手URDF文件的編寫 我們用urdf文件來描述我們的機械手的外觀以及物理性能。這里為了簡便&#xff0c;就只用了基本的圓柱、立方體了。追求美觀的朋友&#xff0c;還可以用dae文件來描述機械手的外形。 import re def remove_comments(text):pattern r<!--(.*?)-->…

《構建社交應用的安全結界:雙框架對接審核API的底層邏輯與實踐》

用戶生成內容如潮水般涌來。從日常的生活分享&#xff0c;到激烈的觀點碰撞&#xff0c;這些內容賦予社交應用活力&#xff0c;也帶來管理難題。虛假信息、暴力言論、侵權內容等不良信息&#xff0c;如同潛藏的暗礁&#xff0c;威脅著社交平臺的健康生態。內容審核機制&#xf…

39:分類器流程

第一步 創建支持向量機分類器 create_class_svm (7, rbf, KernelParam, Nu, |ClassNames|, one-versus-one, principal_components, 5, SVMHandle) 第二步 添加樣本到分類器里 for ClassNumber : 0 to |ClassNames| - 1 by 1 *列出目錄下的所有文件 list_files (ReadPath…

LangChain對話鏈:打造智能多輪對話機器人

LangChain對話鏈:打造智能多輪對話機器人 目錄 LangChain對話鏈:打造智能多輪對話機器人ConversationChain 是什么核心功能與特點基本用法示例內存機制自定義提示詞應用場景與其他鏈的結合`SequentialChain` 是什么![在這里插入圖片描述](https://i-blog.csdnimg.cn/direct/0…

el-select 結合 el-tree:樹形下拉數據

一、單選 <template><div class"selectTree-wapper"><el-selectv-model"selectValue"placeholder"請選擇"popper-class"custom-el-select-class"ref"selectRef"clearableclear"clearHandle">&…

BFS算法篇——從晨曦到星辰,BFS算法在多源最短路徑問題中的詩意航行(下)

文章目錄 引言一、01矩陣1.1 題目鏈接&#xff1a;https://leetcode.cn/problems/01-matrix/description/1.2 題目分析&#xff1a;1.3 思路講解&#xff1a;1.4 代碼實現&#xff1a; 二、飛地的數量2.1 題目鏈接&#xff1a;https://leetcode.cn/problems/number-of-enclaves…

Leetcode (力扣)做題記錄 hot100(49,136,169,20)

力扣第49題&#xff1a;字母異位詞分組 49. 字母異位詞分組 - 力扣&#xff08;LeetCode&#xff09; 遍歷數組&#xff0c;將每一個字符串變成char數組 然后排序&#xff0c;如果map里面有則將他的值返回來&#xff08;key是排序好的字符串&#xff09; class Solution {pu…

【自學30天掌握AI開發】第1天 - 人工智能與大語言模型基礎

自學30天掌握AI開發 - 第1天 &#x1f4c6; 日期和主題 日期&#xff1a;第1天 主題&#xff1a;人工智能與大語言模型基礎 &#x1f3af; 學習目標 了解人工智能的發展歷史和基本概念掌握大語言模型的基本原理和工作機制區分不同類型的AI模型及其特點理解AI在當前社會中的…

WebRTC 源碼原生端Demo入門-1

1、概述 我的代碼是比較新的&#xff0c;基于webrtc源碼倉庫的main分支的&#xff0c;在windows下把源碼倉庫下載好了后&#xff0c;用visual stdio 2022打開進行編譯調試src/examples/peerconnection_client測試項目,主要是跑通這個demo來入手和調試&#xff0c;純看代碼很難…

【LeetCode】刪除排序數組中的重復項 II

題目 鏈接 思路 雙指針 我好聰明啊&#xff0c;自己想出了這個雙指針的辦法&#xff0c;哈哈哈哈哈哈哈&#xff0c;太高興了 代碼 class Solution(object):def removeDuplicates(self, nums):""":type nums: List[int]:rtype: int"""nlen…

通義千問席卷日本!開源界“卷王”阿里通義千問成為日本AI發展新基石

據日本經濟新聞&#xff08;NIKKEI&#xff09;報道&#xff0c;通義千問已成為日本AI開發的新基礎&#xff0c;其影響力正逐步擴大&#xff0c;深刻改變著日本AI產業的格局。 同時&#xff0c;日本經濟新聞將通義千問Qwen2.5-Max列為全球AI模型綜合評測第六名&#xff0c;不僅…

第J7周:對于ResNeXt-50算法的思考

目錄 思考 一、代碼功能分析 1. 構建 shortcut 分支&#xff08;殘差連接的旁路&#xff09; 2. 主路徑的第一層卷積&#xff08;11&#xff09; 4. 主路徑的第三層卷積&#xff08;11&#xff09; 5. 殘差連接 激活函數 二、問題分析總結&#xff1a;殘差結構中通道數不一致的…

如何解決Jmeter中的亂碼問題?

在 JMeter 中遇到亂碼問題通常是由于字符編碼不一致導致的&#xff0c;常見于 HTTP 請求響應、參數化文件讀取、報告生成等場景。以下是系統化的解決方案&#xff1a; 1. HTTP 請求響應亂碼 原因&#xff1a; 服務器返回的字符編碼&#xff08;如UTF-8、GBK&#xff09;與 J…

# YOLOv2:目標檢測的升級之作

YOLOv2&#xff1a;目標檢測的升級之作 在目標檢測領域&#xff0c;YOLO&#xff08;You Only Look Once&#xff09;系列算法以其高效的速度和創新的檢測方式受到了廣泛關注。今天&#xff0c;我們就來深入探討一下 YOLOv2&#xff0c;看看它是如何在繼承 YOLOv1 的基礎上進行…

小白入!WiFi 技術大解析

WiFi&#xff0c;全稱Wireless Fidelity&#xff0c;是一種無線局域網技術&#xff0c;允許電子設備通過無線電波連接到互聯網。以下是對WiFi的一些介紹&#xff1a; 一、基本概述 定義&#xff1a;WiFi是一種基于IEEE 802.11標準系列的無線局域網技術&#xff0c;使設備能夠…

【prometheus+Grafana篇】基于Prometheus+Grafana實現windows操作系統的監控與可視化

&#x1f4ab;《博主主頁》&#xff1a; &#x1f50e; CSDN主頁 &#x1f50e; IF Club社區主頁 &#x1f525;《擅長領域》&#xff1a;擅長阿里云AnalyticDB for MySQL(分布式數據倉庫)、Oracle、MySQL、Linux、prometheus監控&#xff1b;并對SQLserver、NoSQL(MongoDB)有了…

推薦一個感覺非常好的文章,是知識圖譜的

為了省瀏覽的事兒&#xff0c;以后打算寫文章都短一些&#xff0c;這樣不用被強制登錄、關注了 正文 鏈接是 https://blog.csdn.net/Appleyk/article/details/80422055 放個截圖 推薦理由 兩個&#xff0c;第一內容確實硬核。第二算是緣分吧&#xff0c;我之前公司好像&am…

《企業級前端部署方案:Jenkins+MinIO+SSH+Gitee+Jenkinsfile自動化實踐》

文章目錄 前言前端項目CICD時序圖一、環境準備1、服務器相關2、Jenkins憑據3、注意事項 二、設計思想1. 模塊化設計2.多環境支持3. 制品管理4. 安全部署機制5. 回滾機制 三、CI階段1、構建節點選擇2、代碼拉取3、代碼編譯4、打包并上傳至minio 四、CD階段五、回滾階段六、構建通…

Go語言超時控制方案全解析:基于goroutine的優雅實現

一、引言 在構建高可靠的后端服務時&#xff0c;超時控制就像是守護系統穩定性的"安全閥"&#xff0c;它確保當某些操作無法在預期時間內完成時&#xff0c;系統能夠及時止損并釋放資源。想象一下&#xff0c;如果沒有超時控制&#xff0c;一個簡單的數據庫查詢卡住…

WTK6900C-48L:離線語音芯片重構玩具DNA,從“按鍵操控”到“聲控陪伴”的交互躍遷

一&#xff1a;開發背景 隨著消費升級和AI技術進步&#xff0c;傳統玩具的機械式互動已難以滿足市場需求。語音控制芯片的引入使玩具實現了從被動玩耍到智能交互的跨越式發展。通過集成高性價比的語音識別芯片&#xff0c;現代智能玩具不僅能精準響應兒童指令&#xff0c;還能實…