SpringBoot+Redis?BitMap實現(xiàn)簽到與統(tǒng)計的項目實踐
最近項目里需要集成簽到和統(tǒng)計功能,連續(xù)簽到后會給用戶發(fā)放一些優(yōu)惠券和獎品,以此來吸引用戶持續(xù)在該品臺進(jìn)行活躍。下面我們一些來聊一聊目前主流的實現(xiàn)方案。
因為簽到和統(tǒng)計的功能涉及的數(shù)據(jù)量比較大,所以在如此大的數(shù)據(jù)下利用傳統(tǒng)的關(guān)系型數(shù)據(jù)庫進(jìn)行計算和統(tǒng)計是非常耗費性能的,所以目前市面上主要依賴于高性能緩存RedisBitMap 功能來實現(xiàn)。
先看看利用Mysql實現(xiàn)以上功能會有哪些缺陷和短板。
1.使用Mysql實現(xiàn)簽到功能
首先我們需要一個簽到表
DROP TABLE IF EXISTS `tb_sign`; CREATE TABLE `tb_sign` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `user_id` int(11) NOT NULL COMMENT '用戶Id', `year` year(4) NOT NULL COMMENT '簽到的年', `month` tinyint(2) NOT NULL COMMENT '簽到的月', `date` date NOT NULL COMMENT '簽到日期', `is_backup` tinyint(1) NOT NULL COMMENT '是否補簽', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
用戶一次簽到,就是一條記錄,假如有1000萬用戶,平均每人每年簽到次數(shù)為10次,則這張表一年的數(shù)據(jù)量為 1億條
每簽到一次需要使用(8 + 8 + 1 + 1 + 3 + 1)共22 字節(jié)的內(nèi)存,一個月則最多需要600多字節(jié)
這樣的壞處,占用內(nèi)存太大了,極大的消耗內(nèi)存空間!
我們可以根據(jù) Redis中 提供的 BitMap 位圖功能來實現(xiàn),每次簽到與未簽到用0 或1 來標(biāo)識 ,一次存31個數(shù)字,只用了2字節(jié) 這樣我們就用極小的空間實現(xiàn)了簽到功能
2.Redis BitMap
2.1 BitMap 的操作指令
- SETBIT :向指定位置(offset)存入一個0或1
- GETBIT :獲取指定位置(offset)的bit值
- BITCOUNT :統(tǒng)計BitMap中值為1的bit位的數(shù)量
- BITFIELD :操作(查詢、修改、自增)BitMap中bit數(shù)組中的指定位置(offset)的值
- BITFIELD_RO :獲取BitMap中bit數(shù)組,并以十進(jìn)制形式返回
- BITOP :將多個BitMap的結(jié)果做位運算(與 、或、異或)
- BITPOS :查找bit數(shù)組中指定范圍內(nèi)第一個0或1出現(xiàn)的位置
2.2 使用 BitMap 完成功能實現(xiàn)
利用SETBIT新增key 進(jìn)行存儲
SETBIT bm1 0 1
看不懂上面的指令?沒關(guān)系,我們可以通過help指令查看提示
help SETBIT

通過這個指令可以看出Redis SETBIT 命令用于對 key 所儲存的字符串值,設(shè)置或清除指定偏移量上的位(bit)。位的設(shè)置或清除取決于 value,可以是 0 或者是 1 。
當(dāng) key 不存在時,自動生成一個新的字符串值。字符串會進(jìn)行伸展以確保它可以將 value 保存在指定的偏移量上。當(dāng)字符串值進(jìn)行伸展時,空白位置以 0 填充。 offset 參數(shù)必須大于或等于 0 ,小于 2^32 (bit 被限制在 512 MB 之內(nèi))。
提示:如果 offset 偏移量的值較大,計算機進(jìn)行內(nèi)存分配時可能會造成 Redis 服務(wù)器被阻塞。

這樣的話我們就可以通過偏移量設(shè)置每一天的簽到情況:
- 偏移量:表示天
- val值:表示是否簽到
- 已簽到設(shè)置為1
- 未簽到設(shè)置0
下面我們只需要通過GETBIT命令就可以查看每一天的簽到情況
GETBIT bm1 2
表示查看bm1用戶第二天的簽到情況

同樣,我們可以通過BITCOUNT可以統(tǒng)計出該用戶簽到了多少天
BITCOUNT bm1

BITFIELD
通過BITFIELD以原子方式操作(查詢、修改、自增)BitMap中bit數(shù)組中的指定位置(offset)的值
BITFIELD復(fù)雜度是O(n),其中n是訪問的計數(shù)器數(shù)。
語法:
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
參數(shù)說明:
- key :需要操作的鍵名。
- GET type offset :獲取指定位數(shù)的值
- type表示數(shù)據(jù)類型,可以是無符號整數(shù)(u)或有符號整數(shù)(i)
- offset表示偏移量,如果位不存在,返回0。
- SET type offset value :設(shè)置指定位數(shù)的值,type和offset同上,value表示需要設(shè)置的值,因為此命令只支持設(shè)置8位或更少的位,所以value不能超過8個二進(jìn)制位。
- INCRBY type offset increment :將指定位的值增加指定的增量,type和offset同上,increment表示需要增加的值,也必須不超過8個二進(jìn)制位。
- OVERFLOW :做溢出的處理,默認(rèn)是WRAP(循環(huán)),其他兩個選項是SAT(飽和,超出范圍的值都設(shè)置成最大或最小值)和FAIL(不允許溢出,會返回一個錯誤)。
使用方法
- 獲取一個整數(shù)的二進(jìn)制位
舉個例子,我們有一個整數(shù)值10,它的二進(jìn)制位是00001010,我們想獲取它的第3位到第5位的值,即001。那么可以這樣使用:
127.0.0.1:6379> BITFIELD myint GET u3 3 u1 6 1) (integer) 1 2) (integer) 0
u3:表示3位無符號整數(shù)
指定myint鍵值,通過GET命令獲取它的第3位到第5位,結(jié)果返回一個二進(jìn)制數(shù)001,也就是十進(jìn)制的1。第二個返回值是0,表示其他未指定的位都是0。
- 設(shè)置一個整數(shù)的二進(jìn)制位
現(xiàn)在我們想把剛才的整數(shù)值10的第3位到第5位修改為101,即值為5??梢赃@樣設(shè)置:
127.0.0.1:6379> BITFIELD myint SET u3 3 5 (integer) 10
執(zhí)行成功后,該鍵值指定的整數(shù)值變?yōu)?3(二進(jìn)制為00001101)。
所以統(tǒng)計每個用戶的簽到情況和積分,可以使用 Redis BITFIELD 命令記錄每個用戶每天的簽到情況。
每個用戶一年365天,需要使用整數(shù)類型的BITFIELD信息記錄,每個用戶需要365個二進(jìn)制位來表示簽到情況(已簽到為1,未簽到為0),再需要一個整數(shù)位去表示用戶的總積分,就可以方便地統(tǒng)計用戶簽到情況并進(jìn)行排名。
當(dāng)然該命令的功能遠(yuǎn)不止于此,在某些系統(tǒng)中,需要對某些數(shù)據(jù)做計數(shù),比如對每個IP地址訪問次數(shù)的計數(shù)??梢允褂米址愋偷挠嫈?shù)器來達(dá)到這個目的,首先在Redis中創(chuàng)建一個字符串類型的計數(shù)器(值為0),通過INCRBY命令執(zhí)行增減操作,每次給IP地址所代表的計數(shù)器加1,最后獲取到的長度即為IP地址對應(yīng)的訪問次數(shù)。如果訪問量過大,可以使用整數(shù)類型的BITFIELD存儲計數(shù)器,通過INCRBY命令執(zhí)行增減操作。這樣可以優(yōu)化性能并減少內(nèi)存占用。
BITPOS 查詢1 和 0 第一次出現(xiàn)的坐標(biāo)

3.SpringBoot整合Redis實現(xiàn)簽到功能
3.1 思路分析
考慮到每月初需要重置連續(xù)簽到次數(shù),我們可以把 年和月 作為BitMap的key,然后保存到一個BitMap中,每次簽到就到對應(yīng)的位上把數(shù)字從0 變?yōu)?,只要是1,就代表是這一天簽到了,反之咋沒有簽到。
key的格式為:“USER_SIGN_KEY:” + userId + keySuffix
- USER_SIGN_KEY:前綴
- userId:用戶id
- keySuffix:yyyyMM
Value則采用長度為4個字節(jié)(32位)的位圖(最大月份只有31天)。位圖的每一位代表一天的簽到,1表示已簽,0表示未簽。
例如:USER_SIGN_KEY:122101:202309:表示ID=122101的用戶在2023年9月的簽到記錄
# 用戶9月17號簽到 SETBIT USER_SIGN_KEY:122101:202309 16 1 # 偏移量是從0開始,所以要把17減1 # 檢查9月17號是否簽到 GETBIT USER_SIGN_KEY:122101:202309 16 # 偏移量是從0開始,所以要把17減1 # 統(tǒng)計9月份的簽到次數(shù) BITCOUNT USER_SIGN_KEY:122101:202309 # 獲取9月份前30天的簽到數(shù)據(jù),u30表示取0-29位的數(shù)據(jù) BITFIELD USER_SIGN_KEY:122101:202309 get u30 0 # 獲取9月份首次簽到的日期 BITPOS USER_SIGN_KEY:122101:202309 1 # 返回的首次簽到的偏移量,加上1即為當(dāng)月的某一天
有了上面的理論支撐,下面我開始在項目里去集成,為了節(jié)約篇幅,只展示核心代碼,項目地址在文章末尾,完整代碼可下載之后自行觀看
首先需要我們項目里集成redis,可以參考:《springBoot集成redis(jedis)詳解》
3.2 簽到功能
簽到功能實現(xiàn)方式如下:
@GetMapping("sign")
public Object sign() {
//1. 獲取登錄用戶
Long userId = 122101L;
//2.獲取簽到使用的key
String key = RedisKeyUtils.createSignKey(userId);
//3. 獲取今天是本月的第幾天設(shè)置偏移量
LocalDateTime now = LocalDateTime.now();
int offset = now.getDayOfMonth() - 1;
System.out.println(String.format("入?yún)ⅲ簁ey:%s,offset:%s", JSON.toJSONString(key), JSON.toJSONString(offset)));
//5. 寫入redis setbit key offset 1
Boolean res = jedis.setbit(key, offset, true);
System.out.println(String.format("出參:%s", JSON.toJSONString(res)));
return String.format("%s簽到成功,簽到結(jié)果:%s", JSON.toJSONString(now.format(DateTimeFormatter.ofPattern("yyyyMM"))), JSON.toJSONString(res));
}測試:


簽到功能我們已經(jīng)實現(xiàn),只需要將對應(yīng)該月的某天設(shè)置為1則表示簽到成功
檢查用戶是否簽到
@GetMapping("checkSign")
public Object checkSign(@RequestParam(value = "sigDate") String sigDate) {
//1. 獲取登錄用戶
Long userId = 122101L;
//2.獲取簽到使用的key
LocalDateTime time = TimeUtils.str2LocalDateTime(TimeUtils.PATTERN.YYYYMMDD, sigDate);
if (Objects.isNull(time)) {
return false;
}
String key = RedisKeyUtils.createSignKey(time, userId);
//3. 獲取今天是本月的第幾天設(shè)置偏移量
int offset = time.getDayOfMonth() - 1;
System.out.println(String.format("入?yún)ⅲ簁ey:%s,offset:%s", JSON.toJSONString(key), JSON.toJSONString(offset)));
Boolean res = jedis.getbit(key, offset);
System.out.println(String.format("出參:%s", JSON.toJSONString(res)));
return res;
}訪問:http://127.0.0.1:8080/checkSign?sigDate=2023-09-07

下面我們繼續(xù)看看簽到統(tǒng)計功能
3.3 簽到統(tǒng)計功能
Q1:什么叫連續(xù)簽到天數(shù)?
從最后一次簽到開始向前統(tǒng)計,直到遇到第一次未簽到為止,計算總的簽到次數(shù),就是連續(xù)簽到天數(shù)。

所以我們統(tǒng)計的方式很簡單:
?獲得當(dāng)前這個月的最后一次簽到數(shù)據(jù),定義一個計數(shù)器,然后不停的向前統(tǒng)計,直到獲得第一個非0的數(shù)字即可,每得到一個非0的數(shù)字計數(shù)器+1,直到遍歷完所有的數(shù)據(jù),就可以獲得當(dāng)前月的簽到總天數(shù)了?
Q2:如何得到本月到今天為止的所有簽到數(shù)據(jù)?
那我們則需要借助BITFIELD指令來進(jìn)行實現(xiàn)
BITFIELD key GET u[dayOfMonth-1] 0
假設(shè)今天是5號,那么我們就可以從當(dāng)前月的第一天開始,獲得到當(dāng)前這一天的位數(shù),是5號,那么就是5位,去拿這段時間的數(shù)據(jù),就能拿到所有的數(shù)據(jù)了,那么這5天里邊簽到了多少次呢?統(tǒng)計有多少個1即可。
Q3:如何從后向前遍歷每個Bit位?
值得我們注意的是:?bitMap返回的數(shù)據(jù)是10進(jìn)制,哪假如說返回一個數(shù)字8,那么我哪兒知道到底哪些是0,哪些是1呢??
這是一道很簡單的位運算算法題
我們只需要讓得到的10進(jìn)制數(shù)字和1做與運算就可以了,因為1只有遇見1 才是1,其他數(shù)字都是0 ,我們把簽到結(jié)果和1進(jìn)行與操作,每與一次,就把簽到結(jié)果向右移動一位,依次類推,我們就能完成逐個遍歷的效果了。
通過上面的幾個方法就可以很容易統(tǒng)計用戶簽到的情況。
下面看看代碼的具體實現(xiàn)
- 獲取用戶連續(xù)簽到的天數(shù)
為了方便測試,我的當(dāng)前時間為:2023-09-07,所以我對1,3,5,6,7五天的簽到設(shè)置為1

那么當(dāng)前緩存里的值則為:1010111,對應(yīng)的10進(jìn)制數(shù)為:87,最大連續(xù)1出現(xiàn)的個數(shù)為3,所以簽到的天數(shù)為3

代碼如下:
@GetMapping("signCount")
public Object signCount() {
//1. 獲取登錄用戶
Long userId = 122101L;
//2. 獲取日期
LocalDateTime now = LocalDateTime.now();
//3. 拼接key
String key = RedisKeyUtils.createSignKey(now, userId);
//4. 獲取今天是本月的第幾天
int offset = now.getDayOfMonth();
//5. 獲取本月截至今天為止的所有的簽到記錄,返回的是一個十進(jìn)制的數(shù)字 BITFIELD USER_SIGN_KEY1:5:202309 GET u5 0
String type = String.format("u%d", offset);
System.out.println(String.format("入?yún)ⅲ簁ey:%s,operationType:%s,type:%s", JSON.toJSONString(key), JSON.toJSONString(RedisHelper.OPERATION_TYPE.GET.name()), JSON.toJSONString(type)));
List<Long> result = RedisHelper.bitField(key, RedisHelper.OPERATION_TYPE.GET, type, "0");
//沒有任務(wù)簽到結(jié)果
if (result == null || result.isEmpty()) {
return 0;
}
Long num = result.get(0);
if (num == null || num == 0) {
return 0;
}
//6. 循環(huán)遍歷
int count = 0;
while (true) {
//6.1 讓這個數(shù)字與1 做與運算,得到數(shù)字的最后一個bit位 判斷這個數(shù)字是否為0
if ((num & 1) == 0) {
//如果為0,簽到結(jié)束
break;
} else {
count++;
}
num >>>= 1;
}
System.out.println(String.format("緩存值:%s,簽到天數(shù):%d", JSON.toJSONString(result), count));
return count;
}測試:

3.4 獲取當(dāng)月首次簽到日期
利用Bitpos可以幫助我們輕松實現(xiàn)該功能

- bitpos USER_SIGN_KEY:122101:202309 1:表示1首次出現(xiàn)的位置
- bitpos USER_SIGN_KEY:122101:202309 0:表示0首次出現(xiàn)的位置
@GetMapping("getFirstSignDate")
public Object getFirstSignDate(@RequestParam(value = "time") String time) {
//1. 獲取登錄用戶
Long userId = 122101L;
//2. 獲取日期
LocalDateTime dateTime = TimeUtils.str2LocalDateTime(TimeUtils.PATTERN.YYYYMMDD, time);
//3. 拼接key
String key = RedisKeyUtils.createSignKey(dateTime, userId);
//4. 獲取首次出現(xiàn)的位置索引
long pos = jedis.bitpos(key, true);
//5. 轉(zhuǎn)換為日期
String res = pos < 0 ? "無簽到記錄" : TimeUtils.localDateTime2String(TimeUtils.PATTERN.YYYYMMDD, dateTime.withDayOfMonth((int) (pos + 1)));
return res;
}
3.5 獲取當(dāng)月的簽到情況
@GetMapping("getSignInfo")
public Object getSignInfo(@RequestParam(value = "time") String time) {
//1. 獲取登錄用戶
Long userId = 122101L;
//2. 獲取日期
LocalDateTime dateTime = TimeUtils.str2LocalDateTime(TimeUtils.PATTERN.YYYYMMDD, time);
//當(dāng)月天數(shù)
int monthOfDays = TimeUtils.getDayNumsOfMonth(dateTime);
Map<String, Boolean> signMap = new HashMap<>(dateTime.getDayOfMonth());
//3. 拼接key
String key = RedisKeyUtils.createSignKey(dateTime, userId);
//4. 設(shè)置位數(shù)
String type = String.format("u%d", monthOfDays);
System.out.println(String.format("入?yún)ⅲ簁ey:%s,operationType:%s,type:%s", JSON.toJSONString(key), JSON.toJSONString(RedisHelper.OPERATION_TYPE.GET.name()), JSON.toJSONString(type)));
List<Long> list = RedisHelper.bitField(key, RedisHelper.OPERATION_TYPE.GET, type, "0");
if (!CollectionUtils.isEmpty(list)) {
Long num = list.get(0);
int i = monthOfDays;
while (i > 0) {
String d = TimeUtils.localDateTime2String(TimeUtils.PATTERN.YYYYMMDD, dateTime.withDayOfMonth(i--));
if ((num & 1) == 0) {
signMap.put(d, false);
} else {
signMap.put(d, true);
}
num >>>= 1;
}
}
//按照日期排序
TreeMap sortedMap = new TreeMap(signMap);
System.out.println("出參:" + JSON.toJSONString(sortedMap));
return sortedMap;
}
至此簽到所需要的功能我們就全部實現(xiàn)了
本文的核心在于bitMap的使用,但是任何技術(shù)都需要有具體的落地,所以借著簽到的場景帶著大家具體使用一下。
當(dāng)然bitmap的落地場景遠(yuǎn)不止我上文中介紹的簽到統(tǒng)計等業(yè)務(wù)層面的方案,針對緩存穿透的場景,bitMap也是一個極佳的選擇。
4.利用BitMap解決緩存穿透
描述: 緩存穿透是指緩存和數(shù)據(jù)庫中都沒有的數(shù)據(jù),而用戶不斷發(fā)起請求,如發(fā)起為id為“-1”的數(shù)據(jù)或id為特別大不存在的數(shù)據(jù)。這時的用戶很可能是攻擊者,攻擊會導(dǎo)致數(shù)據(jù)庫壓力過大。
這種可以認(rèn)為是系統(tǒng)漏洞,一旦發(fā)生這種情況一般都來自于惡意請求。
常見解決方案:
- 接口層增加校驗,如用戶鑒權(quán)校驗,id做基礎(chǔ)校驗,id<=0的直接攔截;
- 從緩存取不到的數(shù)據(jù),在數(shù)據(jù)庫中也沒有取到,這時也可以將key-value對寫為key-null,緩存有效時間可以設(shè)置短點,如30秒(設(shè)置太長會導(dǎo)致正常情況也沒法使用)。這樣可以防止攻擊用戶反復(fù)用同一個id暴力攻擊
第一種解決方案:遇到的問題是如果用戶訪問的是id不存在的數(shù)據(jù),則此時就無法生效,如id遠(yuǎn)大于某個值,雖然不小于0,但是也無法進(jìn)行過濾。
第二種解決方案:遇到的問題是:如果是不同的id那就可以防止下次過來直擊數(shù)據(jù)
所以我們?nèi)绾谓鉀Q呢?
我們可以將數(shù)據(jù)庫的數(shù)據(jù),所對應(yīng)的id寫入到一個list集合中,當(dāng)用戶過來訪問的時候,我們直接去判斷l(xiāng)ist中是否包含當(dāng)前的要查詢的數(shù)據(jù),如果說用戶要查詢的id數(shù)據(jù)并不在list集合中,則直接返回,如果list中包含對應(yīng)查詢的id數(shù)據(jù),則說明不是一次緩存穿透數(shù)據(jù),則直接放行。

現(xiàn)在的問題是這個主鍵其實并沒有那么短,有的可能是通過各種拼接生成的Id,是很長的一個主鍵,所以如果采用以上方案,這個list也會很大,所以我們可以 使用bitmap來減少list的存儲空間
我們可以把list數(shù)據(jù)抽象成一個非常大的bitmap,我們不再使用list,而是將db中的id數(shù)據(jù)利用哈希思想,比如:
id 求余bitmap長度 : index=id%bitmap.size
算出當(dāng)前這個id對應(yīng)應(yīng)該落在bitmap的哪個索引上,然后將這個值從0變成1,然后當(dāng)用戶來查詢數(shù)據(jù)時,此時已經(jīng)沒有了list,讓用戶用他查詢的id去用相同的哈希算法, 算出來當(dāng)前這個id應(yīng)當(dāng)落在bitmap的哪一位,然后判斷這一位是0,還是1,如果是0則表明這一位上的數(shù)據(jù)一定不存在,采用這種方式來處理,需要重點考慮一個事情,就是誤差率, 所謂的誤差率就是指當(dāng)發(fā)生哈希沖突的時候,產(chǎn)生的誤差 。

項目地址:https://gitee.com/ninesuntec/redisBitMapSign
到此這篇關(guān)于SpringBoot+Redis BitMap實現(xiàn)簽到與統(tǒng)計的項目實踐的文章就介紹到這了,更多相關(guān)SpringBoot+Redis BitMap簽到與統(tǒng)計內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot項目Redis統(tǒng)計在線用戶的實現(xiàn)示例
- SpringBoot+Redis Bitmap實現(xiàn)活躍用戶統(tǒng)計
- 微服務(wù)Spring Boot 整合 Redis 實現(xiàn)UV 數(shù)據(jù)統(tǒng)計的詳細(xì)過程
- 微服務(wù)?Spring?Boot?整合?Redis?BitMap?實現(xiàn)?簽到與統(tǒng)計功能
- SpringBoot整合Redis實現(xiàn)訪問量統(tǒng)計的示例代碼
- SpringBoot使用Redis的zset統(tǒng)計在線用戶信息
- SpringBoot運用Redis統(tǒng)計用戶在線數(shù)量的兩種方法實現(xiàn)
相關(guān)文章
Java中Double除保留后小數(shù)位的幾種方法(小結(jié))
這篇文章主要介紹了Java中Double保留后小數(shù)位的幾種方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07
springboot 自定義LocaleResolver實現(xiàn)切換語言
我們在做項目的時候,往往有很多項目需要根據(jù)用戶的需要來切換不同的語言,使用國際化就可以輕松解決。這篇文章主要介紹了springboot 自定義LocaleResolver切換語言,需要的朋友可以參考下2019-10-10
Spring ApplicationContextAware 接口的作用及使用方式
Spring提供了許多回調(diào)接口,用于Bean生命周期中執(zhí)行特定的操作,通過實現(xiàn)ApplicationContextAware接口,Spring提供了一種便捷的方式讓 Bean獲取對Spring容器的引用,本文介紹ApplicationContextAware接口的作用、使用方式,以及在實際應(yīng)用中的常見場景,感興趣的朋友一起看看吧2024-01-01
Java使用Thread和Runnable的線程實現(xiàn)方法比較
這篇文章主要介紹了Java使用Thread和Runnable的線程實現(xiàn)方法,結(jié)合實例形式對比分析了Java使用Thread和Runnable實現(xiàn)與使用線程的相關(guān)操作技巧,需要的朋友可以參考下2019-10-10
我從jdk1.8升級到j(luò)dk11所遇到的坑都有這些
這篇文章主要介紹了從jdk1.8升級到j(luò)dk11將會遇到的一些坑,本文給大家分享解決方案對大家的學(xué)習(xí)或工作具有參考借鑒價值,對jdk1.8升級到j(luò)dk11相關(guān)知識感興趣的朋友,快來看看吧2021-08-08
Java Poi 在Excel中輸出特殊符號的實現(xiàn)方法
這篇文章主要介紹了Java Poi 在Excel中輸出特殊符號的實現(xiàn)方法,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07

