春節(jié)將至傲茄,又快到了一年一度搶紅包的激動時刻盘榨。
為此呢草巡,我專門針對想要學(xué)習(xí)java山憨,或剛開始學(xué)習(xí)java的小白們郁竟,寫了一段簡單易懂的【春節(jié)搶紅包】代碼棚亩,其中涉及到部分的java編程基礎(chǔ)知識。也涉及到關(guān)于真正搶紅包的思考嘹屯。相信你們一定能有所收貨州弟,同時又能有所聯(lián)想婆翔。
請想要學(xué)習(xí)的同學(xué)們仔閱讀代碼的注釋啃奴,有部分基礎(chǔ)知識的講解我沒有單獨抽取最蕾,放在注釋當中了瘟则。雖然本文不會針對每個知識點講到原理醋拧,但是會在下面列舉出來提綱丹壕,這些是以后工作中常用菌赖,且面試涉及幾率很高的內(nèi)容盏袄,希望各位看完本篇后辕羽,能夠針對這些內(nèi)容加強理解刁愿。
一铣口、基礎(chǔ)知識
內(nèi)部涉及到的基礎(chǔ)知識提綱如下脑题,我會針對每個知識點簡單介紹:
-
單例模式
單例模式解決了兩個問題:- 保證一個類只有一個實例
- 為該實例提供一個全局訪問節(jié)點叔遂。
單例模式包含多種實現(xiàn)方式:比如“餓漢模式”(本文所使用的方式)已艰、“懶漢模式”(線程安全/DCL)哩掺、靜態(tài)內(nèi)部類嚼吞、枚舉等等舱禽。
本文使用餓漢模式呢蔫,原因在于:線程安全片吊,類加載完成就完成了實例化俏脊,簡單實用爷贫,推薦漫萄。
想了解更多單例模式的實現(xiàn)請看:http://www.reibang.com/p/6b4d83b71826腾务。
-
AtomicInteger
除了這個類之外岩瘦,java.util.concurrent.atomic包下都是這一類启昧。這是一個小的類工具包密末,支持在單變量上進行無鎖線程安全編程苏遥。
通過CAS(compare and swap)提供數(shù)據(jù)的原子更新师抄。 -
CAS(compare and swap叨吮,比較并替換)
又叫做自旋鎖茶鉴,是以無鎖的方式解決變量原子性問題涵叮。是一種樂觀鎖思想割粮,通過自身的不斷重試舀瓢。CAS 的底層是 lock cmpxchg 指令(X86 架構(gòu))京髓,在單核 CPU 和多核 CPU 下都能夠保證【比較-交換】的原子性堰怨。
在多核狀態(tài)下,某個核執(zhí)行到帶 lock 的指令時诬烹,CPU 會讓總線鎖住绞吁,當這個核把此指令執(zhí)行完畢家破,再開啟總線汰聋。這個過程中不會被線程的調(diào)度機制所打斷烹困,保證了多個線程對內(nèi)存操作的準確性拟蜻,是原子的酝锅。
特點:無鎖方式搔扁,線程不會阻塞阁谆。效率高。如果競爭過于激烈嫉入,極大增加重試次數(shù)咒林,效率反而降低。
-
lombok.Data (注解@Data)
lombok能極大的幫助我們提高代碼的開發(fā)效率欢瞪,其提供了一系列的注解遣鼓,使我們的代碼更為整潔骑祟。- @AllArgsConstructor
生成全參數(shù)的構(gòu)造怯晕,在配合到spring/springboot的構(gòu)造器注入時舟茶,可是不寫其構(gòu)造方法:
@AllArgsConstructor public class ItemStorageAppServiceImpl implements IItemStorageAppService { private ElasticSearchRepository elastic;
有同學(xué)文為什么不使用@Autowired稚晚?當然可以使用,只是構(gòu)造方法的注入方式是官方推薦的注入方式也搓。
@Data
作用于類上傍妒,是以下注解的集合:@ToString @EqualsAndHashCode @Getter @Setter @RequiredArgsConstructor颤练;也是我比較喜歡使用的注解。@Getter/Setter
作用于類上宇挫,生成成員變量的get和set方法器瘪。
關(guān)于其他的橡疼,各位同學(xué)自行學(xué)習(xí)啊衰齐。
- @AllArgsConstructor
-
BigDecimal的常用方式
BigDecimal是涉及到金額問題的最常用解決方式废酷,所以熟練使用其方法很重要澈蟆,包括加(add)減(subtract)操作趴俘、比較(compareTo)等等。必會類型疲憋,不多說缚柳。
-
LinkedList的特點
應(yīng)該是早起最常見的面試題了吧,和ArrayList相比有什么不同灰追。同學(xué)們后面自己找相關(guān)資料仔細學(xué)習(xí)下弹澎。
簡單來說LinkedList是順序的桐猬,鏈表的結(jié)構(gòu),使得其添加/刪除數(shù)據(jù)更快音五,因為不涉及到數(shù)據(jù)遷移的問題;線程不安全坚嗜;查詢數(shù)據(jù)相比于ArrayList要慢苍蔬,需要遍歷查找。
-
三目運算符的常用方式
這個沒有什么好說的格仲,常見的代碼編寫方式:
envelopeLogCache.get(redEnvelopeDO.getId()) == null ? new HashMap<>(4) : envelopeLogCache.get(redEnvelopeDO.getId());
如上的例子表示凯肋,envelopeLogCache根據(jù)edEnvelopeDO.getId()獲取一個Object,這個Object獲取到了嗎苗桂?如果是null,就給它一個new 的HashMap便锨,如果不是空放案,就把這個對象Object返回掸冤。
自行體會稿湿。
-
多線程場景下的數(shù)據(jù)異常
在本文的代碼示例當中,就出現(xiàn)了嚴重的多線程場景下無法保證數(shù)據(jù)的原子性問題涕俗。這一類問題在多線程、高并發(fā)場景是必然會涉及到的枕稀。
在java當中有對應(yīng)的JUC(java.util.concurrent)類庫去解決這一系列的問題询刹,是java學(xué)習(xí)者必須會的知識。可以參考我的文集:http://www.reibang.com/nb/51656252萎坷,當前持續(xù)更新中凹联。
-
java8 lambda表達式
java8新出現(xiàn)的流式編程方式,極大的縮減代碼復(fù)雜度哆档,使其更加符合面向?qū)ο蟮恼Z義。
上面的內(nèi)容很基礎(chǔ)瓜浸,但是都比較重要的澳淑,無論是寫代碼,閱讀源碼插佛,都會涉及杠巡,我只是再次提供一些思路。
二雇寇、編碼開始
從現(xiàn)在開始氢拥,正式進入編碼階段,全部代碼有三個實體類锨侯,兩個實現(xiàn)類嫩海,一個全局變量類,一個初始化處理器類囚痴。另外有兩個是業(yè)務(wù)實現(xiàn)的接口叁怪,但是因為我們用main方法做的此次演示,暫時忽略吧深滚。
2.1 實體類
- 用戶類
import lombok.Data;
import java.math.BigDecimal;
/**
* @description: 人
* @author:weirx
* @date:2022/1/6 9:40
* @version:3.0
*/
@Data
public class PeopleDO {
/**
* 搶紅包人的id
*/
private Integer id;
/**
* 人名
*/
private String name;
/**
* 金額
*/
private BigDecimal amount;
public PeopleDO(Integer id, String name, BigDecimal amount) {
this.id = id;
this.name = name;
this.amount = amount;
}
}
- 紅包類
import lombok.Data;
import java.math.BigDecimal;
/**
* @description: 紅包
* @author:weirx
* @date:2022/1/6 9:37
* @version:3.0
*/
@Data
public class RedEnvelopeDO {
/**
* 紅包id
*/
private Integer id;
/**
* 紅包名稱
*/
private String name;
/**
* 金額
*/
private BigDecimal amount;
/**
* 數(shù)量
*/
private Integer quantity;
/**
* 發(fā)紅包人的id
*/
private Integer peopleId;
public RedEnvelopeDO(String name, BigDecimal amount, Integer quantity, Integer peopleId) {
this.name = name;
this.amount = amount;
this.quantity = quantity;
this.peopleId = peopleId;
}
}
- 搶紅包歷史記錄類
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* @description: 搶紅包記錄
* @author:weirx
* @date:2022/1/6 9:45
* @version:3.0
*/
@Data
public class GrabEnvelopeLogDO {
/**
* 用戶id
*/
private Integer peopleId;
/**
* 紅包id
*/
private Integer redEnvelopeId;
/**
* 搶到的金額
*/
private BigDecimal amount;
/**
* 發(fā)送時間
*/
private Date createTime;
public GrabEnvelopeLogDO(Integer peopleId, Integer redEnvelopeId, BigDecimal amount, Date createTime) {
this.peopleId = peopleId;
this.redEnvelopeId = redEnvelopeId;
this.amount = amount;
this.createTime = createTime;
}
}
實體類使用 @Data 注解奕谭,需要引用如下依賴:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
這個注解使我們在開發(fā)過程中可以極大的增加開發(fā)效率耳璧,我此處使用可以讓我們省去寫get、set方法的繁瑣展箱。調(diào)用時又不耽誤我們正常使用。
另外就是根據(jù)不同的實體類業(yè)務(wù)場景蹬昌,我們可以創(chuàng)建不同參數(shù)列表的構(gòu)造器混驰,這是java“重載”在構(gòu)造器的體現(xiàn)。
2.2 實現(xiàn)類
共有兩個實現(xiàn)類皂贩,分別是發(fā)送紅包的實現(xiàn)類和搶紅包的工具類:
- 發(fā)送紅包實現(xiàn)類
import com.cloud.bssp.chinesenewyear.grabredenvelope.GlobalDataCache;
import com.cloud.bssp.chinesenewyear.grabredenvelope.GlobalInitProcessor;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.PeopleDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.RedEnvelopeDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.service.ISendRedEnvelopeService;
import java.math.BigDecimal;
import java.util.Map;
/**
* @description: 發(fā)紅包接口
* @author:weirx
* @date:2022/1/6 9:56
* @version:3.0
*/
public class SendRedEnvelopeServiceImpl implements ISendRedEnvelopeService {
@Override
public RedEnvelopeDO send(RedEnvelopeDO redEnvelopeDO) throws Exception {
// 獲取全局變量
GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();
//獲取用戶信息
PeopleDO peopleDO = globalDataCache.getPeopleCache().get(redEnvelopeDO.getPeopleId());
//校驗用戶金額是否足夠發(fā)紅包
if (peopleDO.getAmount().compareTo(redEnvelopeDO.getAmount()) < 0) {
// 直接跑出異常栖榨,調(diào)用時捕獲異常內(nèi)容,實際應(yīng)該定制通用返回結(jié)果明刷,并且有對象的成功失敗標識
throw new Exception("您當前余額不足婴栽,紅包發(fā)送失敗");
}
// 扣除用戶賬戶金額
BigDecimal subtract = peopleDO.getAmount().subtract(redEnvelopeDO.getAmount());
peopleDO.setAmount(subtract);
// 將用戶存入緩存當中
Map<Integer, PeopleDO> peopleCache = globalDataCache.getPeopleCache();
peopleCache.put(peopleDO.getId(), peopleDO);
globalDataCache.setPeopleCache(peopleCache);
// 獲取全局唯一紅包id,并將紅包存入緩存
redEnvelopeDO.setId(globalDataCache.getId());
Map<Integer, RedEnvelopeDO> redEnvelopeCache = globalDataCache.getRedEnvelopeCache();
redEnvelopeCache.put(redEnvelopeDO.getId(), redEnvelopeDO);
globalDataCache.setRedEnvelopeCache(redEnvelopeCache);
return redEnvelopeDO;
}
}
- 搶紅包實現(xiàn)類
import cn.hutool.core.util.ObjectUtil;
import com.cloud.bssp.chinesenewyear.grabredenvelope.GlobalDataCache;
import com.cloud.bssp.chinesenewyear.grabredenvelope.GlobalInitProcessor;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.GrabEnvelopeLogDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.PeopleDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.RedEnvelopeDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.service.IGrabRedEnvelopeService;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @description: 搶紅包
* @author:weirx
* @date:2022/1/6 10:58
* @version:3.0
*/
public class GrabRedEnvelopeServiceImpl implements IGrabRedEnvelopeService {
@Override
public void grab(Integer peopleId, Integer redEnvelopeId) throws Exception {
// 獲取全局變量
GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();
// 獲取用戶
Map<Integer, PeopleDO> peopleCache = globalDataCache.getPeopleCache();
PeopleDO peopleDO = peopleCache.get(peopleId);
// 獲取紅包
Map<Integer, RedEnvelopeDO> redEnvelopeCache = globalDataCache.getRedEnvelopeCache();
RedEnvelopeDO redEnvelopeDO = redEnvelopeCache.get(redEnvelopeId);
// 獲取紅包歷史
Map<Integer, Map<Integer, GrabEnvelopeLogDO>> envelopeLogCache = globalDataCache.getEnvelopeLogCache();
Map<Integer, GrabEnvelopeLogDO> integerGrabEnvelopeLogDOMap =
envelopeLogCache.get(redEnvelopeDO.getId()) == null ? new HashMap<>(4)
: envelopeLogCache.get(redEnvelopeDO.getId());
//判斷紅包是否還有余量
if (redEnvelopeDO.getQuantity() > 0) {
// 計算搶到的紅包金額,并減去余額,和可搶數(shù)量
BigDecimal sub = this.sub(redEnvelopeDO);
// 用戶增加余額
peopleDO.setAmount(peopleDO.getAmount().add(sub));
// 記錄搶紅包歷史
// 沒有紅包歷史則新建辈末,有則返回不能搶紅包
if (ObjectUtil.isNotEmpty(integerGrabEnvelopeLogDOMap) &&
ObjectUtil.isNotEmpty(integerGrabEnvelopeLogDOMap.get(peopleId))) {
throw new Exception("很抱歉愚争,您已搶過紅包");
} else {
GrabEnvelopeLogDO grabEnvelopeLog = new GrabEnvelopeLogDO(peopleId, redEnvelopeId, sub, new Date());
integerGrabEnvelopeLogDOMap.put(peopleId, grabEnvelopeLog);
envelopeLogCache.put(redEnvelopeId, integerGrabEnvelopeLogDOMap);
globalDataCache.setEnvelopeLogCache(envelopeLogCache);
}
} else {
throw new Exception("很抱歉,紅包已被搶完挤聘!");
}
}
/**
* description: 計算搶到的紅包金額,并減去余額
*
* @param redEnvelopeDO
* @return: BigDecimal
* @author: weirx
* @time: 2022/1/6 11:06
*/
private BigDecimal sub(RedEnvelopeDO redEnvelopeDO) {
BigDecimal scale;
if (redEnvelopeDO.getQuantity() > 1) {
// 計算能獲取的最金額轰枝,指定最大最小范圍
int max = redEnvelopeDO.getAmount().intValue();
double min = 0.01;
// 隨機范圍,不會超過max组去,也不會小于min
BigDecimal db = new BigDecimal(Math.random() * (max - min) + min);
//保留兩位小數(shù)鞍陨,不四舍五入
scale = db.setScale(2, BigDecimal.ROUND_DOWN);
} else {
// 剩一個則獲取全部
scale = redEnvelopeDO.getAmount();
}
//設(shè)置紅包余額
redEnvelopeDO.setAmount(redEnvelopeDO.getAmount().subtract(scale));
//設(shè)置剩余可搶數(shù)量
redEnvelopeDO.setQuantity(redEnvelopeDO.getQuantity() - 1);
return scale;
}
}
上面的實現(xiàn)都對應(yīng)實現(xiàn)了其各自的接口,我也提供下从隆,暫時沒有使用:
import org.springframework.stereotype.Service;
/**
* @description: 搶紅包接口
* @author:weirx
* @date:2022/1/6 9:52
* @version:3.0
*/
@Service
public interface IGrabRedEnvelopeService {
/**
* description: 搶紅包接口
*
* @param peopleId 用戶id
* @param redEnvelopeId 紅包id
* @author: weirx
* @time: 2022/1/6 9:54
*/
void grab(Integer peopleId, Integer redEnvelopeId) throws Exception;
}
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.RedEnvelopeDO;
import org.springframework.stereotype.Service;
/**
* @description: 發(fā)送紅包的接口
* @author:weirx
* @date:2022/1/6 9:49
* @version:3.0
*/
@Service
public interface ISendRedEnvelopeService {
/**
* description: 發(fā)送紅包
*
* @param redEnvelopeDO 紅包
* @return: RedEnvelopeDO
* @author: weirx
* @time: 2022/1/6 9:50
*/
RedEnvelopeDO send(RedEnvelopeDO redEnvelopeDO) throws Exception;
}
關(guān)于代碼的詳細解釋都在注釋里面了诚撵,我就不在單獨解釋了。
這里好像使用了hutool工具键闺,一時疏忽寿烟,但是既然用了就給大家提一嘴,需要引入下面的依賴:
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.18</version>
</dependency>
這是一個大而全的工具類艾杏,只要你想的到的韧衣,基本在這里面都有對應(yīng)的工具類。不要錯過购桑。
2.3 全局變量類及初始化處理器
全局變量類是我走的一個臨時存儲數(shù)據(jù)的類畅铭,本文沒有使用數(shù)據(jù)庫等存儲組件。也正是由于這個原因?qū)е铝硕嗑€程的問題勃蜘,后面會介紹硕噩。
- 全局變量類
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.GrabEnvelopeLogDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.PeopleDO;
import com.cloud.bssp.chinesenewyear.grabredenvelope.entity.RedEnvelopeDO;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @description: 全局數(shù)據(jù)存儲(為了方便學(xué)習(xí),我沒有使用任何的數(shù)據(jù)庫等缭贡,直接通過內(nèi)存當時存儲)
* @author:weirx
* @date:2022/1/6 10:01
* @version:3.0
*/
@Data
public class GlobalDataCache {
/**
* AtomicInteger是原子性的炉擅,能夠保證在并發(fā)環(huán)境下的數(shù)據(jù)原子性
* 通過CAS(compare and swap辉懒,比較并替換)實現(xiàn)的原子性
*
* 我使用這個變量作為后面對象的自增id
*/
private final AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 存儲全部用戶數(shù)據(jù)
*/
private Map<Integer, PeopleDO> peopleCache = new HashMap<>();
/**
* 存儲全部紅包數(shù)據(jù)
*/
private Map<Integer, RedEnvelopeDO> redEnvelopeCache = new HashMap<>();
/**
* 紅包歷史數(shù)據(jù) Map<紅包id, Map<用戶id,GrabEnvelopeLogDO>>
*/
private Map<Integer, Map<Integer, GrabEnvelopeLogDO>> envelopeLogCache = new HashMap<>();
/**
* 獲取全局唯一id
*
* incrementAndGet方法使用CAS自加1,保證原子性
*
* @return
*/
public Integer getId() {
return atomicInteger.incrementAndGet();
}
}
- 全局變量初始化處理器
/**
* @description: 全局初始化處理器
* @author:weirx
* @date:2022/1/6 10:12
* @version:3.0
*/
public class GlobalInitProcessor {
/**
* 單例模式(使用靜態(tài)變量實現(xiàn))谍失,個人認為是最好也是最實用的方式眶俩。
*
* 靜態(tài)變量在程序運行時就會加載,從而執(zhí)行g(shù)etInstance方法快鱼,創(chuàng)建對象實例颠印,有且僅有一次創(chuàng)建的過程
*
* 調(diào)用方使用GlobalInitProcessor.getGlobalDataCache()即可獲取GlobalDataCache的全局唯一實例
*/
private final static GlobalDataCache GLOBAL_DATA_CACHE = getInstance();
private static GlobalDataCache getInstance() {
return new GlobalDataCache();
}
public static GlobalDataCache getGlobalDataCache() {
return GLOBAL_DATA_CACHE;
}
}
2.4 main方法測試
基礎(chǔ)的代碼都在前面的小節(jié)提供了,本小節(jié)偶爾們主要做些實驗看結(jié)果抹竹。
- 求和工具類(用于看結(jié)果正確性)
/**
* description: 計算總金額
*
* @param globalDataCache
* @return: void
* @author: weirx
* @time: 2022/1/6 15:02
*/
public static void sum(GlobalDataCache globalDataCache) {
//計算被領(lǐng)取紅包的總金額
final BigDecimal[] count = {new BigDecimal(0)};
globalDataCache.getEnvelopeLogCache().forEach((k, v) -> {
v.forEach((k1, v1) -> {
count[0] = count[0].add(v1.getAmount());
});
});
System.out.println("被搶總金額:" + count[0]);
//計算每個人的錢包總金額
final BigDecimal[] count1 = {new BigDecimal(0)};
globalDataCache.getPeopleCache().forEach((k, v) -> {
count1[0] = count1[0].add(v.getAmount());
});
System.out.println("所有人的總金額:" + count1[0]);
//紅包剩余金額
globalDataCache.getRedEnvelopeCache().forEach((k, v) -> {
System.out.println("紅包剩余金額:" + v.getAmount());
});
}
- 三人順序搶紅包
指定張三线罕、李四、王五三個人窃判,其中每個人各自一百塊钞楼;由張三發(fā)送【新年快樂】紅包,總金額100袄琳,共三個人可以搶询件,代碼如下:
public static void main(String[] args) {
GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();
// 準備用戶
PeopleDO zhangsan = new PeopleDO(globalDataCache.getId(), "張三", new BigDecimal(100));
PeopleDO lisi = new PeopleDO(globalDataCache.getId(), "李四", new BigDecimal(100));
PeopleDO wangwu = new PeopleDO(globalDataCache.getId(), "王五", new BigDecimal(100));
Map<Integer, PeopleDO> map = new HashMap<>(4);
map.put(zhangsan.getId(), zhangsan);
map.put(lisi.getId(), lisi);
map.put(wangwu.getId(), wangwu);
globalDataCache.setPeopleCache(map);
//張三發(fā)100的紅包
SendRedEnvelopeServiceImpl sendRedEnvelopeService = new SendRedEnvelopeServiceImpl();
RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO("新年快樂", new BigDecimal(100), 3, zhangsan.getId());
try {
sendRedEnvelopeService.send(redEnvelopeDO);
} catch (Exception e) {
System.out.println(e.getMessage());
}
//張三、李四唆樊、王五按【順序】搶紅包
LinkedList<Integer> userList = new LinkedList<>();
userList.add(zhangsan.getId());
userList.add(lisi.getId());
userList.add(wangwu.getId());
GrabRedEnvelopeServiceImpl grabRedEnvelopeService = new GrabRedEnvelopeServiceImpl();
userList.forEach(i -> {
try {
grabRedEnvelopeService.grab(i, redEnvelopeDO.getId());
} catch (Exception e) {
System.out.println(e.getMessage());
}
});
System.out.println(globalDataCache);
sum(globalDataCache);
}
代碼簡單雳殊,直接看結(jié)果了:
GlobalDataCache(atomicInteger=4, peopleCache={1=PeopleDO(id=1, name=張三, amount=32.64), 2=PeopleDO(id=2, name=李四, amount=125.06), 3=PeopleDO(id=3, name=王五, amount=142.30)}, redEnvelopeCache={4=RedEnvelopeDO(id=4, name=新年快樂, amount=0.00, quantity=0, peopleId=1)}, envelopeLogCache={4={1=GrabEnvelopeLogDO(peopleId=1, redEnvelopeId=4, amount=32.64, createTime=Thu Jan 06 17:03:21 CST 2022), 2=GrabEnvelopeLogDO(peopleId=2, redEnvelopeId=4, amount=25.06, createTime=Thu Jan 06 17:03:21 CST 2022), 3=GrabEnvelopeLogDO(peopleId=3, redEnvelopeId=4, amount=42.30, createTime=Thu Jan 06 17:03:21 CST 2022)}})
被搶總金額:100.00
所有人的總金額:300.00
紅包剩余金額:0.00
- 減少紅包數(shù)為兩個
RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO("新年快樂", new BigDecimal(100), 2, zhangsan.getId());
結(jié)果當中有一個人,王五是搶不到的
很抱歉窗轩,紅包已被搶完夯秃!
GlobalDataCache(atomicInteger=4, peopleCache={1=PeopleDO(id=1, name=張三, amount=34.74), 2=PeopleDO(id=2, name=李四, amount=165.26), 3=PeopleDO(id=3, name=王五, amount=100)}, redEnvelopeCache={4=RedEnvelopeDO(id=4, name=新年快樂, amount=0.00, quantity=0, peopleId=1)}, envelopeLogCache={4={1=GrabEnvelopeLogDO(peopleId=1, redEnvelopeId=4, amount=34.74, createTime=Thu Jan 06 17:06:20 CST 2022), 2=GrabEnvelopeLogDO(peopleId=2, redEnvelopeId=4, amount=65.26, createTime=Thu Jan 06 17:06:20 CST 2022)}})
被搶總金額:100.00
所有人的總金額:300.00
紅包剩余金額:0.00
- 三個人搶四個紅包
RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO("新年快樂", new BigDecimal(100), 4, zhangsan.getId());
結(jié)果:
GlobalDataCache(atomicInteger=4, peopleCache={1=PeopleDO(id=1, name=張三, amount=20.24), 2=PeopleDO(id=2, name=李四, amount=173.39), 3=PeopleDO(id=3, name=王五, amount=100.62)}, redEnvelopeCache={4=RedEnvelopeDO(id=4, name=新年快樂, amount=5.75, quantity=1, peopleId=1)}, envelopeLogCache={4={1=GrabEnvelopeLogDO(peopleId=1, redEnvelopeId=4, amount=20.24, createTime=Thu Jan 06 17:08:47 CST 2022), 2=GrabEnvelopeLogDO(peopleId=2, redEnvelopeId=4, amount=73.39, createTime=Thu Jan 06 17:08:47 CST 2022), 3=GrabEnvelopeLogDO(peopleId=3, redEnvelopeId=4, amount=0.62, createTime=Thu Jan 06 17:08:47 CST 2022)}})
被搶總金額:94.25
所有人的總金額:294.25
紅包剩余金額:5.75
通過上面的測試,我們發(fā)現(xiàn)痢艺,三種情況下總金額都是300仓洼,沒有發(fā)生數(shù)據(jù)原子性的問題,那是因為我們的搶紅包是通過一個線程串行去搶的堤舒,然而實際情況是不可能的色建。
大家在年會搶紅包的時候,都是一直盯著手機舌缤,所以人基本同一時刻點擊搶紅包箕戳,先讓不滿足上述的理想環(huán)境。
2.5 并發(fā)場景下的搶紅包
在本章節(jié)我們使用多線程去模擬多人在公司年會搶紅包国撵,仍然是100的紅包陵吸,模擬10個人搶,總共10個紅包介牙,通過10個線程模擬10個人壮虫,修改測試方法如下:
public static void main(String[] args) throws InterruptedException {
// 定義一個常量,創(chuàng)建的用戶數(shù)环础,也是搶紅包的人數(shù)囚似,同樣是紅包設(shè)定的個數(shù)(此場景就設(shè)置正好的數(shù)量吧)
int num = 10;
//獲取全局變量
GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();
// 準備用戶, Map初始化記得賦予初始大小剩拢,設(shè)置值 * 負載因子(0.75) = 實際容量
Map<Integer, PeopleDO> peopleDOMap = new HashMap<>(128);
for (int i = 0; i < num; i++) {
//循環(huán)初始化用戶,添加到peopleDOMap中
PeopleDO peopleDO = new PeopleDO(globalDataCache.getId(), "用戶:" + i, new BigDecimal(100));
peopleDOMap.put(peopleDO.getId(), peopleDO);
}
// 設(shè)置用戶到全局變量
globalDataCache.setPeopleCache(peopleDOMap);
//實例化發(fā)紅包的業(yè)務(wù)實現(xiàn)
SendRedEnvelopeServiceImpl sendRedEnvelopeService = new SendRedEnvelopeServiceImpl();
//構(gòu)造一個紅包
RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO(
"新年快樂", new BigDecimal(100), num, peopleDOMap.keySet().iterator().next());
try {
// 發(fā)送紅包
sendRedEnvelopeService.send(redEnvelopeDO);
} catch (Exception e) {
//此處會捕獲手動拋出的“用戶余額不足異橙幕剑”
System.out.println(e.getMessage());
}
// 獲取錢紅包實現(xiàn)
GrabRedEnvelopeServiceImpl grabRedEnvelopeService = new GrabRedEnvelopeServiceImpl();
// 使用屏障或者叫同步器徐伐,指定一個數(shù)字,當線程調(diào)用一個await方法募狂,數(shù)字加1呵晨,知道等于設(shè)置的數(shù)值,所有線程才會開始執(zhí)行熬尺,否則一直處于阻塞狀態(tài)。
CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
// 并發(fā)的搶紅包
peopleDOMap.forEach((k, v) -> {
// 創(chuàng)建和人數(shù)一樣多的線程
new Thread(() -> {
try {
// 等到所有線程到達
cyclicBarrier.await();
// 執(zhí)行搶紅包方法
grabRedEnvelopeService.grab(k, redEnvelopeDO.getId());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}).start();
});
// 主線程休眠1秒谓罗,否則業(yè)務(wù)線程還沒執(zhí)行完粱哼,主線程就結(jié)束了,看不到結(jié)果
TimeUnit.SECONDS.sleep(1);
System.out.println(globalDataCache);
// 結(jié)果統(tǒng)計
sum(globalDataCache);
}
結(jié)果:
GlobalDataCache(atomicInteger=11, peopleCache={1=PeopleDO(id=1, name=用戶:0, amount=60.07), 2=PeopleDO(id=2, name=用戶:1, amount=106.39), 3=PeopleDO(id=3, name=用戶:2, amount=125.67), 4=PeopleDO(id=4, name=用戶:3, amount=186.65), 5=PeopleDO(id=5, name=用戶:4, amount=178.62), 6=PeopleDO(id=6, name=用戶:5, amount=180.41), 7=PeopleDO(id=7, name=用戶:6, amount=174.70), 8=PeopleDO(id=8, name=用戶:7, amount=170.05), 9=PeopleDO(id=9, name=用戶:8, amount=170.19), 10=PeopleDO(id=10, name=用戶:9, amount=126.10)}, redEnvelopeCache={11=RedEnvelopeDO(id=11, name=新年快樂, amount=-317.50, quantity=2, peopleId=1)}, envelopeLogCache={11={3=GrabEnvelopeLogDO(peopleId=3, redEnvelopeId=11, amount=25.67, createTime=Thu Jan 06 17:17:44 CST 2022)}})
被搶總金額:25.67
所有人的總金額:1478.85
紅包剩余金額:-317.50
上述結(jié)果大家看到了吧檩咱,這才是10個人揭措,看到數(shù)額完全不對了,紅包剩余都變成了負數(shù)的刻蚯,總金額頁遠超了1000塊绊含,二紅包被搶的總共在25塊多。要是這樣發(fā)紅包的老板要賠死都不知道咋回事啊炊汹。
- 問題分析
我們的數(shù)據(jù)都是存儲在一個共享變量GlobalDataCache當中的躬充,我們首先結(jié)合下圖JMM(java內(nèi)存模型)分析下:
我們共享變量GlobalDataCache在實例化后存儲在堆中,堆是共享的讨便;當線程被創(chuàng)建充甚,并且使用到這個GlobalDataCache時,會從堆中獲取霸褒,然后將其存儲到自己的虛擬機棧當中伴找;虛擬機棧中有棧幀,每個方法就是一個棧幀废菱,棧幀中又包含本地變量表技矮,此時的GlobalDataCache就存在這里面。所以當線程咋方法中修改GlobalDataCache時殊轴,修改的只是本地變量表的數(shù)據(jù)衰倦,沒有修改隊中的數(shù)據(jù),只有當當方法全部完成后旁理,才會同步到隊中的GlobalDataCache耿币。此時必然產(chǎn)生數(shù)據(jù)不同步的問題了。
-
解決方案
優(yōu)點基礎(chǔ)的同學(xué)一定會想到使用鎖實現(xiàn)韧拒,我們在java中常間的鎖有Synchronized淹接,LockSupport以及ReetrantLock十性。想了解原理的同學(xué)可以看我的這個專題:http://www.reibang.com/u/e62c72db32f1
本文使用ReetrantLock來解決問題,所以有修改后的測試方法如下:
public static void main(String[] args) throws InterruptedException {
// 定義一個常量塑悼,創(chuàng)建的用戶數(shù)劲适,也是搶紅包的人數(shù),同樣是紅包設(shè)定的個數(shù)(為了測試紅包不足厢蒜,實際會減少)
int num = 10;
//獲取全局變量
GlobalDataCache globalDataCache = GlobalInitProcessor.getGlobalDataCache();
// 準備用戶, Map初始化記得賦予初始大小霞势,設(shè)置值 * 負載因子(0.75) = 實際容量
Map<Integer, PeopleDO> peopleDOMap = new HashMap<>(128);
for (int i = 0; i < num; i++) {
//循環(huán)初始化用戶,添加到peopleDOMap中
PeopleDO peopleDO = new PeopleDO(globalDataCache.getId(), "用戶:" + i, new BigDecimal(100));
peopleDOMap.put(peopleDO.getId(), peopleDO);
}
// 設(shè)置用戶到全局變量
globalDataCache.setPeopleCache(peopleDOMap);
//實例化發(fā)紅包的業(yè)務(wù)實現(xiàn)
SendRedEnvelopeServiceImpl sendRedEnvelopeService = new SendRedEnvelopeServiceImpl();
//構(gòu)造一個紅包
RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO(
"新年快樂", new BigDecimal(100), num - 2, peopleDOMap.keySet().iterator().next());
try {
// 發(fā)送紅包
sendRedEnvelopeService.send(redEnvelopeDO);
} catch (Exception e) {
//此處會捕獲手動拋出的“用戶余額不足異嘲哐唬”
System.out.println(e.getMessage());
}
// 獲取錢紅包實現(xiàn)
GrabRedEnvelopeServiceImpl grabRedEnvelopeService = new GrabRedEnvelopeServiceImpl();
// 使用屏障或者叫同步器愕贡,指定一個數(shù)字,當線程調(diào)用一個await方法巷屿,數(shù)字加1固以,知道等于設(shè)置的數(shù)值,所有線程才會開始執(zhí)行嘱巾,否則一直處于阻塞狀態(tài)憨琳。
CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
// 保證數(shù)據(jù)原子性,線程同步旬昭,等待線程處于阻塞狀態(tài)篙螟,lock/unlock
ReentrantLock lock = new ReentrantLock();
peopleDOMap.forEach((k, v) -> {
// 創(chuàng)建和人數(shù)一樣多的線程
new Thread(() -> {
try {
// 等到所有線程到達
cyclicBarrier.await();
// 鎖住搶紅包方法grap,此時是互斥的问拘,只有當前線程能進來遍略,其余縣城在阻塞隊列等待
lock.lock();
// 執(zhí)行搶紅包方法
grabRedEnvelopeService.grab(k, redEnvelopeDO.getId());
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
// 釋放互斥鎖,要保證在finally當中執(zhí)行釋放鎖骤坐,防止死鎖發(fā)生
lock.unlock();
}
}).start();
});
// 主線程休眠1秒墅冷,否則業(yè)務(wù)線程還沒執(zhí)行完,主線程就結(jié)束了或油,看不到結(jié)果
TimeUnit.SECONDS.sleep(1);
System.out.println(globalDataCache);
// 結(jié)果統(tǒng)計
sum(globalDataCache);
}
此次我們還減少兩個紅包數(shù)量寞忿,即8個:
RedEnvelopeDO redEnvelopeDO = new RedEnvelopeDO("新年快樂", new BigDecimal(100), num - 2, peopleDOMap.keySet().iterator().next());
結(jié)果:
很抱歉,紅包已被搶完顶岸!
很抱歉腔彰,紅包已被搶完!
GlobalDataCache(atomicInteger=11, peopleCache={1=PeopleDO(id=1, name=用戶:0, amount=4.29), 2=PeopleDO(id=2, name=用戶:1, amount=100.69), 3=PeopleDO(id=3, name=用戶:2, amount=102.21), 4=PeopleDO(id=4, name=用戶:3, amount=101.59), 5=PeopleDO(id=5, name=用戶:4, amount=100.00), 6=PeopleDO(id=6, name=用戶:5, amount=100), 7=PeopleDO(id=7, name=用戶:6, amount=100.00), 8=PeopleDO(id=8, name=用戶:7, amount=100.85), 9=PeopleDO(id=9, name=用戶:8, amount=100), 10=PeopleDO(id=10, name=用戶:9, amount=190.37)}, redEnvelopeCache={11=RedEnvelopeDO(id=11, name=新年快樂, amount=0.00, quantity=0, peopleId=1)}, envelopeLogCache={11={1=GrabEnvelopeLogDO(peopleId=1, redEnvelopeId=11, amount=4.29, createTime=Thu Jan 06 17:41:25 CST 2022), 2=GrabEnvelopeLogDO(peopleId=2, redEnvelopeId=11, amount=0.69, createTime=Thu Jan 06 17:41:25 CST 2022), 3=GrabEnvelopeLogDO(peopleId=3, redEnvelopeId=11, amount=2.21, createTime=Thu Jan 06 17:41:25 CST 2022), 4=GrabEnvelopeLogDO(peopleId=4, redEnvelopeId=11, amount=1.59, createTime=Thu Jan 06 17:41:25 CST 2022), 5=GrabEnvelopeLogDO(peopleId=5, redEnvelopeId=11, amount=0.00, createTime=Thu Jan 06 17:41:25 CST 2022), 7=GrabEnvelopeLogDO(peopleId=7, redEnvelopeId=11, amount=0.00, createTime=Thu Jan 06 17:41:25 CST 2022), 8=GrabEnvelopeLogDO(peopleId=8, redEnvelopeId=11, amount=0.85, createTime=Thu Jan 06 17:41:25 CST 2022), 10=GrabEnvelopeLogDO(peopleId=10, redEnvelopeId=11, amount=90.37, createTime=Thu Jan 06 17:41:25 CST 2022)}})
被搶總金額:100.00
所有人的總金額:1000.00
紅包剩余金額:0.00
由上所示發(fā)現(xiàn)沒有任何問題辖佣。
三霹抛、微服務(wù)常用組件
前面扯了一大堆基礎(chǔ)到不能在基礎(chǔ)的內(nèi)容,下面我們自由飛翔一下卷谈,看看當今企業(yè)中常見的技術(shù)棧有哪些杯拐,可以利用到我們的搶紅包的場景當中。
下面我簡單畫一幅架構(gòu)圖,列出比較常用的架構(gòu)設(shè)計:
如上圖所示端逼,從web請求開始朗兵,涉及到如下組建,咱們逐一舉例:
負載均衡
磁層主要是對網(wǎng)關(guān)做負載均衡顶滩,企業(yè)通常采用軟負載余掖,常用nginx等,或使用F5的負載均衡組件礁鲁。網(wǎng)關(guān)層
網(wǎng)關(guān)在如今的主流java開發(fā)領(lǐng)域盐欺,使用較多的是springCloud生態(tài)的zuul或者gateway組件,自帶負載均衡和代理轉(zhuǎn)發(fā)的功能仅醇。同時可以作為權(quán)限驗證的組件冗美,如集成JWT等。也可以做請求攔截析二,白名單等粉洼。業(yè)務(wù)服務(wù)層
通常就是咱們寫業(yè)務(wù)代碼的一層,目前主流的框架有兩個甲抖,分別是阿里開源的dubbo生態(tài),隨著注冊中心nacos的出現(xiàn)心铃,可以取代原本的zookeeper准谚,目前使用量較廣。另一個就是springCloud的生態(tài)去扣,提供豐富的組件庫柱衔,F(xiàn)eign,ribbon愉棱,eureka,hystrix等等組件,應(yīng)當是目前使用量最廣泛的java微服務(wù)框架存淫。-
數(shù)據(jù)持久層
此層我應(yīng)該再細分為三個層面:- 關(guān)系型數(shù)據(jù)庫:主流的是mysql和PostgreSQL谜喊,傳統(tǒng)行業(yè)可能還在使用Oracle;以及目前阿里背書的OceanBase等朋其。
- 緩存:隨著目前服務(wù)數(shù)量王浴,用戶量的增加,緩存對于互聯(lián)網(wǎng)應(yīng)用來說越來越重要梅猿。主流是redis和MongoDB氓辣,使用量都很廣泛。
- 搜索引擎:主流是Elasticsearch袱蚓、solr等钞啸。業(yè)務(wù)場景對于查詢量大的,修改少的場景也可以使用。
限流体斩、熔斷梭稚、降級
當并發(fā)量很大,系統(tǒng)不足以應(yīng)付的時候硕勿,可以使用這些策略保證系統(tǒng)的可用性哨毁。springcloud提供自帶的組件hystrix。
但是我此處指的是作用與網(wǎng)關(guān)層源武,也就是請求的入口處扼褪。推薦使用阿里開源的Sentinel。注冊中心/配置中心
目前主流的注冊中心在國內(nèi)可以說就是阿里巴巴開源的nacos了粱栖,集服務(wù)發(fā)現(xiàn)和動態(tài)配置于一身话浇,同時支持dubbo和springcloud等主流的微服務(wù)框架。
另外springcloud自帶的eureka暫時不推薦使用了闹究,在易用性上來說完全不如nacos幔崖。還需要單獨選擇一套配置中心搭配,可以使用zookeeper渣淤,etcd等組件自行開發(fā)赏寇;也可以使用攜程的Apollo,也是不錯的配置中心組件价认。消息中間件
消息中間件是目前互聯(lián)網(wǎng)中的明星了嗅定,在解決高并發(fā)量,大吞吐量用踩,異步解耦等方面可以說做到了極致渠退。有其適合大量訂單等場景。
常用的有阿里巴巴的RocketMQ脐彩,RabbitMQ以及經(jīng)久不衰的Kafka碎乃。EFK/ELK
日志收集組件。對于一些大型傳統(tǒng)行業(yè)惠奸,涉及到一些審計的工作梅誓,他們對于操作日志的記錄非常嚴格,此時需要一套專門的系統(tǒng)來做佛南。
主流的有elasticsearch+ fluentd + kibana 和大多數(shù)在企業(yè)使用的elasticsearch+ Logstash + kibana证九。日志存儲組件都是Elasticsearch,查詢組件都是Kibana共虑,主要差別在于日志的收集組件愧怜,這將影響到日志記錄的整體速度,除了上面兩種還有不少的選擇如:flume妈拌、filebeat等-
鏈路追蹤
這個屬于保證系統(tǒng)可用性的組件拥坛,可以反映當前系統(tǒng)的運行狀態(tài)蓬蝶,以及各服務(wù)之間額調(diào)用關(guān)系,甚至于網(wǎng)絡(luò)請求的吞吐量等猜惋⊥璺眨可以集成郵件等組件實現(xiàn)異常告警推送。常用的我推薦skywalking著摔,同時還有zipkin缓窜,和新興的jeager。
Prometheus + Grafana
這是一套云原生的監(jiān)控系統(tǒng)谍咆,可以監(jiān)控服務(wù)器禾锤,服務(wù),以及各種會用到的組件摹察,使用exporter將數(shù)據(jù)收集到prometheus進行存儲恩掷。之后由Grafana提供動態(tài)可配置的報表進行定制展示,是目前最好的監(jiān)控組件供嚎。
關(guān)于常用的組件就介紹到這吧黄娘,當然還有很多很多沒有提到,也還有很多我沒有用過克滴,希望在工作中不斷地學(xué)習(xí)吧逼争。
四、總結(jié)
一篇java入門小知識劝赔,不知道對朋友們有沒有幫助誓焦,碼字不易,給點個贊和關(guān)注啊望忆。
學(xué)完本篇搶紅包的代碼罩阵,保準讓你過年每個紅包都不落下竿秆,每次紅包都搶最大的F羯恪!
小弟提前給你們拜年了S母帧G副浮!