redis并發(fā)扣減庫存(redis并發(fā)鎖incr)

本篇文章給大家談談redis并發(fā)扣減庫存,以及redis并發(fā)鎖incr對應的知識點,文章可能有點長,但是希望大家可以閱讀完,增長自己的知識,最重要的是希望對各位有所幫助...
本篇文章給大家談談redis并發(fā)扣減庫存,以及redis并發(fā)鎖incr對應的知識點,文章可能有點長,但是希望大家可以閱讀完,增長自己的知識,最重要的是希望對各位有所幫助,可以解決了您的問題,不要忘了收藏本站喔。
接口并發(fā)量高的解決方案
這個問題的解決方案是需要是要根據(jù)具體的業(yè)務場景具體分析的
舉例:常見的秒殺系統(tǒng)
1.限流,通過設置服務器的連接等待數(shù)量及等待時間,以tomcat為例,通過設置maxthread的值,當連接數(shù)超過則會放入等待隊列,同時也可設置acceptcount值,若等待數(shù)超過,則會提示連接拒絕
2.引入redis,將秒殺商品數(shù)據(jù)放入redis,用戶點擊搶購,將商品ID去查redis,若商品存在則生成訂單,并保存到緩存,同時庫存-1,減完后判斷商品庫存是否大于0,大于0則更新緩存,否則刪除該商品緩存,并更新庫表(以上步驟僅為單線程操作,需加鎖實現(xiàn),或可考慮采用redis的list對象去實現(xiàn)單線程操作)
3.利用CDN抗壓靜態(tài)頁面流量
為了防止用戶秒殺前不斷刷新產(chǎn)生的流量,可考慮將秒殺商品詳情頁的內(nèi)容靜態(tài)化處理,除了提交訂單,其他數(shù)據(jù)都可緩存在CDN上
除此之外還可引入消息隊列,對非即時響應的服務通過隊列進行解耦
redis預扣庫存如何補償
需要用代碼補償,很簡單,就是個逆向思維,在每次redis執(zhí)行進行回滾之前,校驗一下redis售出庫存==真實售出庫存,如果發(fā)現(xiàn)redis售出庫存!=真實售出庫存,這時候先重置預扣減庫存為真實售出庫存,在進行回滾就實現(xiàn)了redis預扣減的最終一致性。
如何解決秒殺編程高并發(fā)問題
高并發(fā)問題
就是指在同一個時間點,有大量用戶同時訪問URL地址,比如淘寶雙11都會產(chǎn)生高并發(fā)。
高并發(fā)帶來的后果
服務端??導致站點服務器、DB服務器資源被占滿崩潰。??數(shù)據(jù)的存儲和更新結果和理想的設計不一致。用戶角度??尼瑪,網(wǎng)站這么卡,刷新了還這樣,垃圾網(wǎng)站,不玩了二:分析阻礙服務速度的原因1:事物行級鎖的等待
java的事務管理機制會限制在一次commit之前,下一個用戶線程是無法獲得鎖的,只能等待
2:網(wǎng)絡延遲
3:JAVA的自動回收機制(GC)
三:處理高并發(fā)的常見方法
1:首先可以將靜態(tài)資源放入CDN中,減少后端服務器的訪問
2:訪問數(shù)據(jù)使用Redis進行緩存
3:使用Negix實現(xiàn)負載均衡
4:數(shù)據(jù)庫集群與庫表散列
四:實戰(zhàn)優(yōu)化秒殺系統(tǒng)
1:分析原因
當用戶在想秒殺時,秒殺時間未到,用戶可能會一直刷新頁面,獲取系統(tǒng)時間和資源(A:此時會一直訪問服務器),當時間到了,大量用戶同時獲取秒殺接口API(B),獲取API之后執(zhí)行秒殺(C),指令傳輸?shù)礁鞯胤掌鳎掌鲌?zhí)行再將傳遞到中央數(shù)據(jù)庫執(zhí)行(D),服務器啟用事務執(zhí)行減庫存操作,在服務器端JAVA執(zhí)行過程中,可能因為JAVA的自動回收機制,還需要一部分時間回收內(nèi)存(E)。
2:優(yōu)化思路:
面對上面分析可能會影響的過程,我們可以進行如下優(yōu)化
A:我們可以將一些靜態(tài)的資源放到CDN上,這樣可以減少對系統(tǒng)服務器的請求
B:對于暴露秒殺接口,這種動態(tài)的無法放到CDN上,我們可以采用Redis進行緩存
request——>Redis——>MySQL
C:數(shù)據(jù)庫操作,對于MYSQL的執(zhí)行速度大約可以達到1秒鐘40000次,影響速度的還是因為行級鎖,我們應盡可能減少行級鎖持有時間。
DE:對于數(shù)據(jù)庫來說操作可以說是相當快了,我們可以將指令放到MYSQL數(shù)據(jù)庫上去執(zhí)行,減少網(wǎng)絡延遲以及服務器GC的時間。
3:具體實現(xiàn)
3.1:使用Redis進行緩存
引入redis訪問客戶端Jedis
1<!--redis客戶端:Jedis-->2<dependency>3<groupId>redis.clients</groupId>4<artifactId>jedis</artifactId>5<version>2.7.3</version>6</dependency>優(yōu)化暴露秒殺接口:對于SecviceImpl中exportSeckillUrl方法的優(yōu)化,偽代碼如下
getfromcache//首先我們要從Redis中獲取需要暴露的URL
ifnull//如果從Redis中獲取的為空
getdb//那么我們就訪問MYSQL數(shù)據(jù)庫進行獲取
putcache//獲取到后放入Redis中
elselocgoin//否則,則直接執(zhí)行
我們一般不能直接訪問Redis數(shù)據(jù)庫,首先先建立數(shù)據(jù)訪問層RedisDao,RedisDao中需要提供兩個方法,一個是getSeckill和putSeckill
在編寫這兩個方法時還需要注意一個問題,那就是序列化的問題,Redis并沒有提供序列化和反序列化,我們需要自定義序列化,我們使用protostuff進行序列化與反序列化操作
引入protostuff依賴包
1<!--protostuff序列化依賴-->2<dependency>3<groupId>com.dyuproject.protostuff</groupId>4<artifactId>protostuff-core</artifactId>5<version>1.0.8</version>6</dependency>7<dependency>8<groupId>com.dyuproject.protostuff</groupId>9<artifactId>protostuff-runtime</artifactId>10<version>1.0.8</version>11</dependency>編寫數(shù)據(jù)訪問層RedisDao
1packagecom.xqc.seckill.dao.cache;23importorg.slf4j.Logger;4importorg.slf4j.LoggerFactory;56importcom.dyuproject.protostuff.LinkedBuffer;7importcom.dyuproject.protostuff.ProtostuffIOUtil;8importcom.dyuproject.protostuff.runtime.RuntimeSchema;9importcom.xqc.seckill.entity.Seckill;1011importredis.clients.jedis.Jedis;12importredis.clients.jedis.JedisPool;1314/**15*Redis緩存優(yōu)化16*17*@authorACang(xqc)18*19*/20publicclassRedisDao{21privatefinalLoggerlogger=LoggerFactory.getLogger(this.getClass());2223privatefinalJedisPooljedisPool;2425publicRedisDao(Stringip,intport){26jedisPool=newJedisPool(ip,port);27}2829privateRuntimeSchema<Seckill>schema=RuntimeSchema.createFrom(Seckill.class);3031publicSeckillgetSeckill(longseckillId){32//redis操作邏輯33try{34Jedisjedis=jedisPool.getResource();35try{36Stringkey="seckill:"+seckillId;37//并沒有實現(xiàn)內(nèi)部序列化操作38//get->byte[]->反序列化->Object(Seckill)39//采用自定義序列化40//protostuff:pojo.41byte[]bytes=jedis.get(key.getBytes());42//緩存中獲取到bytes43if(bytes!=null){44//空對象45Seckillseckill=schema.newMessage();46ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);47//seckill被反序列化48returnseckill;49}50}finally{51jedis.close();52}53}catch(Exceptione){54logger.error(e.getMessage(),e);55}56returnnull;57}5859publicStringputSeckill(Seckillseckill){60//setObject(Seckill)->序列化->byte[]61try{62Jedisjedis=jedisPool.getResource();63try{64Stringkey="seckill:"+seckill.getSeckillId();65byte[]bytes=ProtostuffIOUtil.toByteArray(seckill,schema,66LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));67//超時緩存68inttimeout=60*60;//1小時69Stringresult=jedis.setex(key.getBytes(),timeout,bytes);70returnresult;71}finally{72jedis.close();73}74}catch(Exceptione){75logger.error(e.getMessage(),e);76}7778returnnull;79}808182}優(yōu)化ServiceImpl的exportSeckillUrl的方法
1publicExposerexportSeckillUrl(longseckillId){2//優(yōu)化點:緩存優(yōu)化:超時的基礎上維護一致性3//1:訪問redis4Seckillseckill=redisDao.getSeckill(seckillId);5if(seckill==null){6//2:訪問數(shù)據(jù)庫7seckill=seckillDao.queryById(seckillId);8if(seckill==null){9returnnewExposer(false,seckillId);10}else{11//3:放入redis12redisDao.putSeckill(seckill);13}14}1516DatestartTime=seckill.getStartTime();17DateendTime=seckill.getEndTime();18//系統(tǒng)當前時間19DatenowTime=newDate();20if(nowTime.getTime()<startTime.getTime()21||nowTime.getTime()>endTime.getTime()){22returnnewExposer(false,seckillId,nowTime.getTime(),startTime.getTime(),23endTime.getTime());24}25//轉化特定字符串的過程,不可逆26Stringmd5=getMD5(seckillId);27returnnewExposer(true,md5,seckillId);28}2930privateStringgetMD5(longseckillId){31Stringbase=seckillId+"/"+salt;32Stringmd5=DigestUtils.md5DigestAsHex(base.getBytes());33returnmd5;34}3.2并發(fā)優(yōu)化:
在執(zhí)行秒殺操作死,正常的執(zhí)行應該如下:先減庫存,并且得到行級鎖,再執(zhí)行插入購買明細,然后再提交釋放行級鎖,這個時候行級鎖鎖住了其他一些操作,我們可以進行如下優(yōu)化,這時只需要延遲一倍。
修改executeSeckill方法如下:
1@Transactional2/**3*使用注解控制事務方法的優(yōu)點:4*1:開發(fā)團隊達成一致約定,明確標注事務方法的編程風格。5*2:保證事務方法的執(zhí)行時間盡可能短,不要穿插其他網(wǎng)絡操作RPC/HTTP請求或者剝離到事務方法外部.6*3:不是所有的方法都需要事務,如只有一條修改操作,只讀操作不需要事務控制.7*/8publicSeckillExecutionexecuteSeckill(longseckillId,longuserPhone,Stringmd5)9throwsSeckillException,RepeatKillException,SeckillCloseException{10if(md5==null||!md5.equals(getMD5(seckillId))){11thrownewSeckillException("seckilldatarewrite");12}13//執(zhí)行秒殺邏輯:減庫存+記錄購買行為14DatenowTime=newDate();1516try{17//記錄購買行為18intinsertCount=successKilledDao.insertSuccessKilled(seckillId,userPhone);19//唯一:seckillId,userPhone20if(insertCount<=0){21//重復秒殺22thrownewRepeatKillException("seckillrepeated");23}else{24//減庫存,熱點商品競爭25intupdateCount=seckillDao.reduceNumber(seckillId,nowTime);26if(updateCount<=0){27//沒有更新到記錄,秒殺結束,rollback28thrownewSeckillCloseException("seckillisclosed");29}else{30//秒殺成功commit31SuccessKilledsuccessKilled=successKilledDao.queryByIdWithSeckill(seckillId,userPhone);32returnnewSeckillExecution(seckillId,SeckillStatEnum.SUCCESS,successKilled);33}34}35}catch(SeckillCloseExceptione1){36throwe1;37}catch(RepeatKillExceptione2){38throwe2;39}catch(Exceptione){40logger.error(e.getMessage(),e);41//所有編譯期異常轉化為運行期異常42thrownewSeckillException("seckillinnererror:"+e.getMessage());43}44}3.3深度優(yōu)化:(存儲過程)
定義一個新的接口,使用存儲過程執(zhí)行秒殺操作
1/**2*執(zhí)行秒殺操作by存儲過程3*@paramseckillId4*@paramuserPhone5*@parammd56*/7SeckillExecutionexecuteSeckillProcedure(longseckillId,longuserPhone,Stringmd5);實現(xiàn)executeSeckillProcedure方法
1publicSeckillExecutionexecuteSeckillProcedure(longseckillId,longuserPhone,Stringmd5){2if(md5==null||!md5.equals(getMD5(seckillId))){3returnnewSeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE);4}5DatekillTime=newDate();6Map<String,Object>map=newHashMap<String,Object>();7map.put("seckillId",seckillId);8map.put("phone",userPhone);9map.put("killTime",killTime);10map.put("result",null);11//執(zhí)行存儲過程,result被復制12try{13seckillDao.killByProcedure(map);14//獲取result15intresult=MapUtils.getInteger(map,"result",-2);16if(result==1){17SuccessKilledsk=successKilledDao.18queryByIdWithSeckill(seckillId,userPhone);19returnnewSeckillExecution(seckillId,SeckillStatEnum.SUCCESS,sk);20}else{21returnnewSeckillExecution(seckillId,SeckillStatEnum.stateOf(result));22}23}catch(Exceptione){24logger.error(e.getMessage(),e);25returnnewSeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);2627}2829}編寫SeckillDao實現(xiàn)有存儲過程執(zhí)行秒殺的邏輯
1/**2*使用存儲過程執(zhí)行秒殺3*@paramparamMap4*/5voidkillByProcedure(Map<String,Object>paramMap);在Mybatis中使用
1<!--mybatis調用存儲過程-->2<selectid="killByProcedure"statementType="CALLABLE">3callexecute_seckill(4#{seckillId,jdbcType=BIGINT,mode=IN},5#{phone,jdbcType=BIGINT,mode=IN},6#{killTime,jdbcType=TIMESTAMP,mode=IN},7#{result,jdbcType=INTEGER,mode=OUT}8)9</select>在Controller層使用
1@ResponseBody2publicSeckillResult<SeckillExecution>execute(@PathVariable("seckillId")LongseckillId,3@PathVariable("md5")Stringmd5,4@CookieValue(value="killPhone",required=false)Longphone){5//springmvcvalid6if(phone==null){7returnnewSeckillResult<SeckillExecution>(false,"未注冊");8}9SeckillResult<SeckillExecution>result;10try{11//存儲過程調用.12SeckillExecutionexecution=seckillService.executeSeckillProcedure(seckillId,phone,md5);13returnnewSeckillResult<SeckillExecution>(true,execution);14}catch(RepeatKillExceptione){15SeckillExecutionexecution=newSeckillExecution(seckillId,SeckillStatEnum.REPEAT_KILL);16returnnewSeckillResult<SeckillExecution>(true,execution);17}catch(SeckillCloseExceptione){18SeckillExecutionexecution=newSeckillExecution(seckillId,SeckillStatEnum.END);19returnnewSeckillResult<SeckillExecution>(true,execution);20}catch(Exceptione){21logger.error(e.getMessage(),e);22SeckillExecutionexecution=newSeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);23returnnewSeckillResult<SeckillExecution>(true,execution);24}25}至此,此系統(tǒng)的代碼優(yōu)化工作基本完成。但是在部署時可以將其更加優(yōu)化,我們一般會使用如下架構
有些程序員一直堅持反對使用redis怎么辦
分享大佬的回答,似乎很有道理。
不要告訴我們用不用redis,你得告訴我們你為什么想要用redis,不用redis業(yè)務會有什么問題?天下沒有免費的午餐,不動腦子直接上緩存/NOSQL可能會帶來更多更嚴重的問題。
單一數(shù)據(jù)庫最大的好處在于事務性實現(xiàn)簡單,由數(shù)據(jù)庫自己保證。舉個簡單的例子,下訂單需要扣除一個庫存,然后插入一條訂單條目,如果庫存和訂單都是數(shù)據(jù)庫表項的話這個事務是無懈可擊的,如果庫存在redis里,訂單條目是MySQL,通常就需要先寫redis,成功之后再寫數(shù)據(jù)庫,如果寫數(shù)據(jù)庫失敗了還需要回滾redis,如果最后這個回滾因為網(wǎng)絡之類的原因失敗了,就會多扣一個庫存。不要以為這些事情很好解決,事務性處理的復雜性遠遠超過你的想象,比如說還有寫MySQL在提交的一瞬間連接斷了這種情況,你都沒法判斷提交到底成功了還是失敗了,那你的redis是回滾還是不回滾?
所以引入新的層一定要說清楚,你為了什么目的一定要用緩存/NOSQL,能接受什么樣的一致性模型,否則就是在胡鬧。
javaWeb 在系統(tǒng)高并發(fā)的情況下生成有序流水號
1,建一個第三方id生成服務器,用rpc調用得到id
2,數(shù)據(jù)庫存一個id種子,每次取出種子,然后把種子及其之后1000條id放入內(nèi)存,種子加1000后馬上回寫數(shù)據(jù)庫,
這樣做的話,如果各服務器在同一時刻用完1000條Id,同時又來取1000條id的時候可能會有并發(fā)問題。但是可能性不大。
3,捕獲id重復異常,碰到異常后再去取一次id
關于redis并發(fā)扣減庫存到此分享完畢,希望能幫助到您。
本文鏈接:http://www.resource-tj.com/qianduan/3423.html