SpringBoot系列教程應用篇之藉助Redis搭建一個簡單站點統計服務
判斷一個網站值不值錢的一個重要標準就是看pv/uv,那麼你知道pv,uv是怎麼統計的麼?當然現在有第三方做的比較完善的可以直接使用,但如果讓我們自己來實現這麼一個功能,應該怎麼做呢?
本篇內容較長,原始碼如右 ➡️ https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/124-redis-sitecount
<!-- more -->
I. 背景及需求
為了看看我的部落格是不是我一個人的單機遊戲,所以就想著統計一下總的訪問量,每日的訪問人數,哪些博文又是大家感興趣的,點選得多的;
因此就萌發了自己擼一個pv/uv統計的服務,當然我這個也不需要特別完善高大上,能滿足我自己的基本需要就可以了
- 希望統計站點(域名)總訪問次數
- 希望統計站點總的訪問人數,當前訪問者在訪問人數中的排名(即這個ip是所有訪問ip中的第多少位訪問的這個站點)
- 每個子頁面都有訪問次數,訪問總人數,當前ip訪問的排名統計
- 同一個ip,同一天內訪問同一個子頁面,pv次數只加1次;隔天之後,再次訪問pv+1
II. 方案設計
前面的背景和需求,可以說大致說明了我們要做個什麼東西,以及需要注意哪些事項,再進行方案設計的過程中,則需要對需求進行詳細拆解
1. 術語說明
前面提到了pv,uv,在我們的實際實現中,會發現這個服務中對於pv,uv的定義和標準定義並不是完全一致的,下面進行說明
a. pv
page viste
但是這裡有個限制: 一個合法的ip,一天之內pv統計次數只能+1次
- 根據ip進行區分,因此需要獲取訪問者ip
- 同一天內,這個ip訪問相同的URI,只能算一次有效pv;第二天之後,再次訪問,則可以再算一次有效pv
b. hot
前面的pv針對ip進行了限制,一個ip同一天的訪問,只能計算一次,大部分情況下這種統計並沒有什麼問題,但是如果一個文章寫得特別有參考意義,導致有人重複的看,仔細的看,換著花樣的重新整理看,這個時候統計下總的訪問次數是不是也挺好的
因此在這個服務中,引入了hot(熱度)的概念,對於一個uri而言,只要一次點選,hot+1
c. uv
unique visitor
, 這個就是統計URI的訪問ip數
2. 流程圖
通過前面三個術語的定義,我們的操作流程就相對清晰了,我們的服務接收一個IP和URI,然後操作對應的pv,uv,hot並返回
- 首先判斷這個ip是否為第一次訪問這個URI
- 是,則pv+1, uv+1, hot+1
- 否,表示之前訪問過,uv就不能變了
- 判斷是否今天第一次訪問
- 是,今天訪問過,那麼pv不變,hot+1
- 否,之前訪問過,今天沒有,pv可以+1, hot+1
對應的流程圖如下
3. 資料結構
流程清晰之後,接下來就需要看下pv,uv,hot三個資料怎麼存了
a. pv
pv儲存的就是訪問次數,與ip無關,所以kv儲存就可以滿足我們的需求了,這裡的key為uri,value則儲存pv的值
b. hot
hot和pv類似,同樣用kv可以滿足要求
c. uv
uv這裡有兩個資料,一個是uv總數,要給是這個ip的訪問排名,redis中有個zset資料結構正好就可以做這個
zset資料結構中,我們定義value為ip,score為ip的排名,那麼uv就是最大的score了
d. 結構圖
4. 方案設計
流程清晰,結構設計出來之後,就可以進入具體的方案設計環節了,在這個環節中,我們引入一個app的維度,這樣我們的服務就可以通用了;
每個使用者都申請一個app,那麼這個使用者的請求的所有站點統計資料,都關聯到這個app上,這樣也有利於後續統計了
a. 介面API
引入了app之後,結合前面的兩個引數ip + URI,我們的請求引數就清晰了
@Data
public class VisitReqDTO {
/**
* 應用區分
*/
private String app;
/**
* 訪問者ip
*/
private String ip;
/**
* 訪問的URI
*/
private String uri;
}
然後我們返回的資料,pv + uv + rank + hot,所以返回的基礎VO如下
/**
* Created by @author yihui in 16:19 19/5/12.
*/
@Data
@AllArgsConstructor
public class VisitVO implements Serializable {
/**
* pv,與傳統的有點區別,這裡表示這個url的總訪問次數;每個ip,一天次數只+1
*/
private Long pv;
/**
* uv 頁面總的ip訪問數
*/
private Long uv;
/**
* 當前ip,第一次訪問本url的排名
*/
private Long rank;
/**
* 熱度,每次訪問計數都+1
*/
private Long hot;
public VisitVO() {
}
public VisitVO(VisitVO visitVO) {
this.pv = visitVO.pv;
this.uv = visitVO.uv;
this.rank = visitVO.rank;
this.hot = visitVO.hot;
}
}
此外需要注意一點的是,發起一個子頁面的請求時,這個時候我們基於域名的站點總數統計也應該被觸發(簡單來說,訪問http://spring.hhui.top/spring-blog/
時,不僅這個uri的統計需要更新, spring.hhui.top
這個域名的pv,uv,hot也需要隨之統計)
因此我們最終的返回物件應該是
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SiteVisitDTO {
/**
* 站點訪問統計
*/
private VisitVO siteVO;
/**
* 頁面訪問統計
*/
private VisitVO uriVO;
}
有輸出,又返回,那麼訪問api就簡單了
SiteVisitDTO visit(VisitReqDTO reqDTO);
b. hot相關api
hot資料結構為hash,每次請求過來,都是次數+1,因此直接使用redis的 hIncrBy
,實現計數+1,並返回最終的計數
- key:
"hot_cnt_" + app
作為hash的key - field: 使用URI作為hash的field
- value: 儲存具體的hot,整型
/**
* 應用的熱度統計計數
*
* @param app
* @return
*/
private String buildHotKey(String app) {
return "hot_cnt_" + app;
}
/**
* 熱度,每訪問一次,計數都+1
*
* @param key
* @param uri
* @return
*/
public Long addHot(String key, String uri);
c. pv相關api
pv與hot不一樣的是並不是每次都需要計數+1,所以它需要有一個查詢pv的介面,和一個計數+1的介面
- key:
"site_cnt_" + app
作為hash的key - field: 使用URI作為hash的field
- value: 儲存具體的pv,整型
/**
* 應用的pv統計計數
*
* @param app
* @return
*/
private String buildPvKey(String app) {
return "site_cnt_" + app;
}
/**
* 獲取pv
*
* pv儲存結果為hash,一個應用一個key; field 為uri; value為pv
*
* @return null表示首次有人訪問;這個時候需要+1
*/
public Long getPv(String key, String uri);
/**
* pv 次數+1
*
* @param key
* @param uri
*/
public void addPv(String key, String uri)
d. uv相關api
前面說到uv採用的是zset資料結構,其中ip作為value,排名作為score;所以uv就是最大的score
- key: 根據app和uri來確定uv的key
- value: 儲存訪問者ip(ipv4格式的)
- score: 排名,整型
因為uv需要返回兩個結構,所以我們的返回需要注意
/**
* app+uri 對應的uv
*
* @param app
* @param uri
* @return
*/
private String buildUvKey(String app, String uri) {
return "uri_rank_" + app + "_" + uri;
}
/**
* 獲取uri對應的uv,以及當前訪問ip的歷史訪問排名
* 使用zset來儲存,key為uri唯一標識;value為ip;score為訪問的排名
*
* @param key : 由app與URI來生成,即一個uri維護一個uv集
* @param ip: 訪問者ip
* @return 返回uv/rank, 如果對應的值為0,表示沒有訪問過
*/
public ImmutablePair</** uv */Long, /** rank */Long> getUv(String key, String ip)
/**
* uv +1
*
* @param key
* @param ip
* @param rank
*/
public void addUv(String key, String ip, Long rank)
e. 今日是否訪問
前面的都還算比較簡單,接下來有個非常有意思的地方了,如何判斷這個ip,今天訪問沒訪問?
方案一
要實現這個功能,一個自然而然的想法就出來了,直接kv就行了
- key:
uri_年月日_ip
- value: 1
如果value存在,表示今天訪問過,如果不存在,則沒有訪問過
方案二
前面那個倒是沒啥問題,如果我希望統計今天某個uri的ip訪問數,上面的就不太好處理,很容易想到用hash來替換
- key:
uri_年月日
- field:
ip
- value: 1
同樣value存在,則表示今天訪問過;否則沒有訪問過
如果需要統計今天訪問的總數,hlen一把就可以;還可以獲取今天所有訪問過的ip
方案三
前面的方案看似挺好的,但是有個缺陷,如果我這個站點特別火,每天幾百萬的uv,這個儲存量就有點誇張了
# 簡單的算一下 10w uv的儲存開銷
field: ip # 一個ip(255.255.255.255) 字串儲存算 16B;
value: 1 # 算 1B
10w uv = 10w * 17B = 1.7MB
# 假設這個站點有100個10w uv的子頁面,每天儲存需要 170MB
通過上面簡單的計算可以看出這儲存開銷對於比較火的站點而言,有點嚇人;然後可以找其他的儲存方式了,所以bitmap可以隆重登場了
我們將位陣列分成四節,分別於ip的四段對應,因為ipv4每一段取值是(0-2^8),所以我們的位陣列,也只需要(4 * 8b = 4B),相比較前面的方案來說,儲存空間大大減少
看到上面這個結構,會有一個疑問,為什麼分成四節?將ip轉成整形,作為下標,一個就可以了
- 答:將ip轉為整型,取值將是 (0 - 2^32),需要的bitmap空間為
4Gb
,顯然不如上面優雅
方案確定
上面三個方案中,我們選擇了第三個,對應的api設計也比較簡單了
// 獲取今天的日期,格式為 20190512
public static String getToday() {
LocalDate date = LocalDate.now();
int year = date.getYear();
int month = date.getMonthValue();
int day = date.getDayOfMonth();
StringBuilder buf = new StringBuilder(8);
return buf.append(year).append(month < 10 ? "0" : "").append(month).append(day < 10 ? "0" : "").append(day)
.toString();
}
/**
* 每日訪問統計
*
* @param app
* @param uri
* @return
*/
private String buildUriTagKey(String app, String uri) {
return "uri_tag_" + DateUtil.getToday() + "_" + app + "_" + uri;
}
/**
* 標記ip訪問過這個key
*
* @param key
* @param ip
*/
public void tagVisit(String key, String ip)
III. 服務實現
前面介面設計出來,按照既定思路實現就屬於比較輕鬆的環節了
1. pv介面實現
pv兩個介面,一個訪問,一個計數+1,都可以直接使用redisTemplate的基礎操作完成
/**
* 獲取pv
*
* pv儲存結果為hash,一個應用一個key; field 為uri; value為pv
*
* @return null表示首次有人訪問;這個時候需要+1
*/
public Long getPv(String key, String uri) {
return redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
byte[] ans = connection.hGet(key.getBytes(), uri.getBytes());
if (ans == null || ans.length == 0) {
return null;
}
return Long.parseLong(new String(ans));
}
});
}
/**
* pv 次數+1
*
* @param key
* @param uri
*/
public void addPv(String key, String uri) {
redisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.hIncrBy(key.getBytes(), uri.getBytes(), 1);
return null;
}
});
}
2. hot介面實現
只有一個計數+1的介面
/**
* 熱度,每訪問一次,計數都+1
*
* @param key
* @param uri
* @return
*/
public Long addHot(String key, String uri) {
return redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
return connection.hIncrBy(key.getBytes(), uri.getBytes(), 1);
}
});
}
3. uv介面實現
uv的獲取會麻煩一點,首先獲取uv值,然後獲取ip對應的排名;如果uv為0,排名也就不需要再獲取了
/**
* 獲取uri對應的uv,以及當前訪問ip的歷史訪問排名
* 使用zset來儲存,key為uri唯一標識;value為ip;score為訪問的排名
*
* @param key : 由app與URI來生成,即一個uri維護一個uv集
* @param ip: 訪問者ip
* @return 返回uv/rank, 如果對應的值為0,表示沒有訪問過
*/
public ImmutablePair</** uv */Long, /** rank */Long> getUv(String key, String ip) {
// 獲取總uv數,也就是最大的score
Long uv = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Set<RedisZSetCommands.Tuple> set = connection.zRangeWithScores(key.getBytes(), -1, -1);
if (CollectionUtils.isEmpty(set)) {
return 0L;
}
Double score = set.stream().findFirst().get().getScore();
return score.longValue();
}
});
if (uv == null || uv == 0L) {
// 表示還沒有人訪問過
return ImmutablePair.of(0L, 0L);
}
// 獲取ip對應的訪問排名
Long rank = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Double score = connection.zScore(key.getBytes(), ip.getBytes());
return score == null ? 0L : score.longValue();
}
});
return ImmutablePair.of(uv, rank);
}
/**
* uv +1
*
* @param key
* @param ip
* @param rank
*/
public void addUv(String key, String ip, Long rank) {
redisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.zAdd(key.getBytes(), rank, ip.getBytes());
return null;
}
});
}
4. 今天是否訪問過
前面選擇位陣列方式來記錄是否訪問過,這裡的實現選擇了簡單的實現方式,利用四個bitmap來分別對應ip的四段;(實際上一個也可以實現,可以想一想應該怎麼做)
/**
* 判斷ip今天是否訪問過
* 採用bitset來判斷ip是否有訪問,key由app與uri唯一確定
*
* @return true 表示今天訪問過/ false 表示今天沒有訪問過
*/
public boolean visitToday(String key, String ip) {
// ip地址進行分段 127.0.0.1
String[] segments = StringUtils.split(ip, ".");
for (int i = 0; i < segments.length; i++) {
if (!contain(key + "_" + i, Integer.valueOf(segments[i]))) {
return false;
}
}
return true;
}
private boolean contain(String key, Integer val) {
return redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.getBit(key.getBytes(), val);
}
});
}
/**
* 標記ip訪問過這個key
*
* @param key
* @param ip
*/
public void tagVisit(String key, String ip) {
String[] segments = StringUtils.split(ip, ".");
for (int i = 0; i < segments.length; i++) {
int finalI = i;
redisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.setBit((key + "_" + finalI).getBytes(), Integer.valueOf(segments[finalI]), true);
return null;
}
});
}
}
4. api介面實現
前面基本的介面實現之後,api就是流程圖的翻譯了,也沒有什麼特別值得說到的地方,唯一需要注意的就是URI的解析,域名作為站點;uri由path + segment構成
public static ImmutablePair</**host*/String, /**uri*/String> foramtUri(String uri) {
URI u = URI.create(uri);
String host = u.getHost();
if (u.getPort() > 0 && u.getPort() != 80) {
host = host + ":80";
}
String baseUri = u.getPath();
if (u.getFragment() != null) {
baseUri = baseUri + "#" + u.getFragment();
}
if (StringUtils.isNotBlank(baseUri)) {
baseUri = host + baseUri;
} else {
baseUri = host;
}
return ImmutablePair.of(host, baseUri);
}
/**
* uri 訪問統計
*
* @param reqDTO
* @return
*/
public SiteVisitDTO visit(VisitReqDTO reqDTO) {
ImmutablePair<String, String> uri = URIUtil.foramtUri(reqDTO.getUri());
// 獲取站點的訪問記錄
VisitVO uriVisit = doVisit(reqDTO.getApp(), uri.getRight(), reqDTO.getIp());
VisitVO siteVisit;
if (uri.getLeft().equals(uri.getRight())) {
siteVisit = new VisitVO(uriVisit);
} else {
siteVisit = doVisit(reqDTO.getApp(), uri.getLeft(), reqDTO.getIp());
}
return new SiteVisitDTO(siteVisit, uriVisit);
}
private VisitVO doVisit(String app, String uri, String ip) {
String pvKey = buildPvKey(app);
String hotKey = buildHotKey(app);
String uvKey = buildUvKey(app, uri);
String todayVisitKey = buildUriTagKey(app, uri);
Long hot = visitService.addHot(hotKey, uri);
// 獲取pv資料
Long pv = visitService.getPv(pvKey, uri);
if (pv == null || pv == 0) {
// 歷史沒有訪問過,則pv + 1, uv +1
visitService.addPv(pvKey, uri);
visitService.addUv(uvKey, ip, 1L);
visitService.tagVisit(todayVisitKey, ip);
return new VisitVO(1L, 1L, 1L, hot);
}
// 判斷ip今天是否訪問過
boolean visit = visitService.visitToday(todayVisitKey, ip);
// 獲取uv及排名
ImmutablePair</**uv*/Long, /**rank*/Long> uv = visitService.getUv(uvKey, ip);
if (visit) {
// 今天訪問過,則不需要修改pv/uv;可以直接返回所需資料
return new VisitVO(pv, uv.getLeft(), uv.getRight(), hot);
}
// 今天沒訪問過
if (uv.left == 0L) {
// 首次有人訪問, pv + 1; uv +1
visitService.addPv(pvKey, uri);
visitService.addUv(uvKey, ip, 1L);
visitService.tagVisit(todayVisitKey, ip);
return new VisitVO(pv + 1, 1L, 1L, hot);
} else if (uv.right == 0L) {
// 這個ip首次訪問, pv +1; uv + 1
visitService.addPv(pvKey, uri);
visitService.addUv(uvKey, ip, uv.left + 1);
visitService.tagVisit(todayVisitKey, ip);
return new VisitVO(pv + 1, uv.left + 1, uv.left + 1, hot);
} else {
// 這個ip的今天第一次訪問, pv + 1 ; uv 不變
visitService.addPv(pvKey, uri);
visitService.tagVisit(todayVisitKey, ip);
return new VisitVO(pv + 1, uv.left, uv.right, hot);
}
}
IV. 測試與小結
1. 測試
搭建一個簡單的web服務,開始測試
/**
* Created by @author yihui in 18:58 19/5/12.
*/
@Controller
public class VisitController {
@Autowired
private SiteVisitFacade siteVisitFacade;
@RequestMapping(path = "visit")
@ResponseBody
public SiteVisitDTO visit(VisitReqDTO reqDTO) {
return siteVisitFacade.visit(reqDTO);
}
}
a. 首次訪問
# 首次訪問,返回的全是1
http://localhost:8080/visit?app=demo&ip=192.168.0.1&uri=http://hhui.top/home
b. 再次訪問
# 再次訪問,因為同樣是今天訪問,除了hot為2;其他的都是1
http://localhost:8080/visit?app=demo&ip=192.168.0.1&uri=http://hhui.top/home
c. 同ip,不同URI
# 同一ip,換個uri;除站點返回hot為3,其他的全是1
http://localhost:8080/visit?app=demo&ip=192.168.0.1&uri=http://hhui.top/index
d. 不同ip,接上一個URI
# 換個ip,這個uri;主站點hot=4, pv,uv,rank=2; uriVO全是2
http://localhost:8080/visit?app=demo&ip=192.168.0.2&uri=http://hhui.top/index
e. 上一個ip,換第一個uri
# 換個ip,這個uri;主站點hot=5, pv,uv,rank=2; uriVO hot為3,其他全是2
http://localhost:8080/visit?app=demo&ip=192.168.0.2&uri=http://hhui.top/home
f. 第二天訪問
真要第二天操作有點麻煩,為了驗證,直接幹掉今天的佔位標記
# 模擬第二天訪問, pv + 1, uv不變, hot+1
http://localhost:8080/visit?app=demo&ip=192.168.0.2&uri=http://hhui.top/home
2. 小結
本文可以說是redis學習之後,一個挺好的應用場景,涉及到了我們常用和不常用的幾個資料結構,包括hash,zset,bitmap, 其中關於bitmap的使用個人感覺還是非常有意思的;
對於redis操作不太熟的,可以參考下前面幾篇博文
- 181029-SpringBoot高階篇Redis之基本配置
- 181101-SpringBoot高階篇Redis之Jedis配置
- 181108-SpringBoot高階篇Redis之String資料結構的讀寫
- 181109-SpringBoot高階篇Redis之List資料結構使用姿勢
- 181202-SpringBoot高階篇Redis之Hash資料結構使用姿勢
- 181211-SpringBoot高階篇Redis之Set資料結構使用姿勢
- 181212-SpringBoot高階篇Redis之ZSet資料結構使用姿勢
- 181225-SpringBoot應用篇之藉助Redis實現排行榜功能
注意
上面這個服務,在實際使用中,需要考慮併發問題,很明顯我們上的設計並不是多執行緒安全的,也就是說,在併發量大的時候,獲取的資料極有可能和預期的不一致
擴充套件
上文的設計中,每個uri都有一組點陣圖,我們可以通過遍歷,獲取value為1的下標,來統計這個頁面今天的pv數,以及更相信的今天哪些ip訪問過;同樣也可以分析站點的今日UV數,以及對應的訪問ip
0. 專案
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 原始碼: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/124-redis-sitecount
1. 一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
- 一灰灰Blog個人部落格 https://blog.hhui.top
- 一灰灰Blog-Spring專題部落格 http://spring.hhui.top
相關推薦
SpringBoot系列教程應用篇之藉助Redis搭建一個簡單站點統計服務
判斷一個網站值不值錢的一個重要標準就是看pv/uv,那麼你知道pv,uv是怎麼統計的麼?當然現在有第三方做的比較完善的可以直接使用
SpringBoot系列教程web篇之Post請求引數解析姿勢彙總
作為一個常年提供各種Http介面的後端而言,如何獲取請求引數可以說是一項基本技能了,本篇為《190824-SpringBoot系列
SpringBoot學習筆記(13)----使用Spring Session+redis實現一個簡單的叢集
session叢集的解決方案: 1.擴充套件指定server 利用Servlet容器提供的外掛功能,自定義HttpSession的建立和管理策略,並通過配置的方式替換掉預設的策略。缺點:耦合Tomcat/Jetty等Servlet容器,不能隨意更換容器。 2.利用Filter 利用Ht
使用SpringBoot+Dubbo 搭建一個簡單的分散式服務
Github 地址: github.com/Snailclimb/… ,歡迎各位 Star。 目錄: 使用 SpringBoot+Dubbo 搭建一個簡單分散式服務 實戰之前,先來看幾個重要的概念 什麼是分散式? 什麼是 Duboo
超詳細,新手都能看懂 !使用SpringBoot+Dubbo 搭建一個簡單的分散式服務
Github 地址:https://github.com/Snailclimb/springboot-integration-examples ,歡迎各位 Star。 目錄: 使用 SpringBoot+Dubbo 搭建一個簡單分散式服務 實戰之前,先來看幾個重要的概念
使用SpringBoot+Dubbo 搭建一個簡單的分散式服務,超詳細,新手都能看懂 !
使用 SpringBoot+Dubbo 搭建一個簡單分散式服務 實戰之前,先來看幾個重要的概念 開始實戰之前,我們先來簡單的瞭解一下這樣幾個概念:Dubbo、RPC、分散式、由於本文的目的是帶大家使用SpringBoot+Dubbo 搭建一個簡單的分散式服務,所以這些概念我只會簡
PHP開發之使用CodeIgniter搭建一個簡單的專案
搭建PHP開發環境請參考PHP開發環境搭建 下載CodeIgniter 新建一個專案 將CodeIgniter中所有檔案copy到專案中。 專案建好了,現在我們要做個簡單的註冊登入功能。 別急,跟我慢慢來。 1.寫sql,建立user表 DRO
SpringBoot系列教程之Redis叢集環境配置
之前介紹的幾篇redis的博文都是基於單機的redis基礎上進行演示說明的,然而在實際的生產環境中,使用redis叢集的可能性應該
Java工程師之SpringBoot系列教程前言&目錄
前言 與時俱進是每一個程式設計師都應該有的意識,當一個Java程式設計師在當代步遍佈的時候,你就行該想到我能多學點什麼。可觀的是後端的框架是穩定的,它們能夠維持更久的時間在應用中,而不用擔心技術的更新換代。但是類似SSH,SSM這些框架已經太久了,人們迫不及待地想使用更為優雅而又簡便的框架來代替,所以Spr
Redis系列-生產應用篇-分散式鎖(5)-單程序Redis分散式鎖的Java實現(Redisson使用與底層實現)-原子鎖類
Redisson單程序Redis分散式樂觀鎖的使用與實現 本文基於Redisson 3.7.5 4. 原子鎖類 Redisson中實現了兩種原子鎖類:RAtomicLong和RAtomicDouble,還有RLongAdder和RDoubleAdder RA
spark系列-應用篇之通過yarn api提交Spark任務
前言 在工作中,大部使用的都是hadoop和spark的shell命令,或者通過java或者scala編寫程式碼。最近工作涉及到通過yarn api處理spark任務,感覺yarn的api還是挺全面的,但是呼叫時需要傳入很多引數,而且會出現一些詭異的問題。雖然
SpringBoot應用篇之FactoryBean及代理實現SPI機制示例
FactoryBean在Spring中算是一個比較有意思的存在了,雖然在日常的業務開發中,基本上不怎麼會用到,但在某些場景下,如果用得好,卻可以實現很多有意思的東西 本篇博文主要介紹如何通過FactoryBean來實現一個類SPI機制的微型應用框架 文章內涉及
SpringBoot系列教程JPA之新增記錄使用姿勢
SpringBoot系列教程JPA之新增記錄使用姿勢 上一篇文章介紹瞭如何快速的搭建一個JPA的專案環境,並給出了一個簡單的
SpringBoot系列教程JPA之基礎環境搭建
JPA(Java Persistence API)Java持久化API,是 Java 持久化的標準規範,Hibernate是持久
SpringBoot系列教程JPA之update使用姿勢
原文: 190623-SpringBoot系列教程JPA之update使用姿勢 上面兩篇博文拉開了jpa使用姿勢的面紗一角,
SpringBoot系列教程之Bean載入順序之錯誤使用姿勢闢謠
在網上查詢 Bean 的載入順序時,看到了大量的文章中使用@Order註解的方式來控制 bean 的載入順序,不知道寫這些的博文的同學自己有沒有實際的驗證過,本文希望通過指出這些錯誤的使用姿勢,讓觀文的小夥伴可以知道@Order的具體的應用場景 原文地址: SpringBoot系列教程之Bean載入順序
SpringBoot系列教程之Bean之指定初始化順序的若干姿勢
上一篇博文介紹了@Order註解的常見錯誤理解,它並不能指定 bean 的載入順序,那麼問題來了,如果我需要指定 bean 的載入順序,那應該怎麼辦呢? 本文將介紹幾種可行的方式來控制 bean 之間的載入順序 構造方法依賴 @DependOn 註解 BeanPostProcessor 擴充套件 原
SpringBoot系列教程JPA之指定id儲存
原文連結: 191119-SpringBoot系列教程JPA之指定id儲存 前幾天有位小夥伴問了一個很有意思的問題,使用 JPA 儲存資料時,即便我指定了主鍵 id,但是新插入的資料主鍵卻是 mysql 自增的 id;那麼是什麼原因導致的呢?又可以如何解決呢? 本文將介紹一下如何使用 JPA 的 AUTO
《手把手教你》系列進階篇之1-python+ selenium自動化測試 - python基礎掃盲(詳細教程)
1. 簡介 如果你從一開始就跟著巨集哥看部落格文章到這裡,基礎篇和練習篇的文章。如果你認真看過,並且手動去敲過每一篇的指令碼程式碼,那邊恭喜你,至少說你算真正會利用Python+Selenium編寫自動化指令碼了,你基本掌握了Selenium中webdriver的大部分常用的方法函式。有大佬曾經說過,自動
《手把手教你》系列進階篇之2-python+ selenium自動化測試 - python基礎掃盲(詳細教程)
1. 簡介 這篇文章主要是分享講解一下,如何封裝自己用到的方法和類。以便方便自己和別人的呼叫,這樣就可以避免重複地再造輪子。 封裝(Encapsulation)是面向物件的三大特徵之一(另外兩個是繼承和多型),它指的是將物件的狀態資訊隱藏在物件內部,不允許外部程式直接訪問物件內部資訊,而是通過該類所提