Redis實(shí)戰(zhàn)(5)-數(shù)據(jù)結(jié)構(gòu)Set實(shí)戰(zhàn)之過(guò)濾用戶注冊(cè)重復(fù)提交的信息
作者:
修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。
摘要:毫無(wú)疑問(wèn),集合Set同樣也是緩存中間件Redis中其中一個(gè)重要的數(shù)據(jù)結(jié)構(gòu),其內(nèi)部存儲(chǔ)的元素/成員具有“唯一”、“隨機(jī)”等特性,在實(shí)際的項(xiàng)目開(kāi)發(fā)中同樣具有相當(dāng)廣泛的應(yīng)用場(chǎng)景。本文我們將介紹并實(shí)戰(zhàn)一種比較典型的業(yè)務(wù)場(chǎng)景~“重復(fù)提交”,即如何利用集合Set的相關(guān)特性實(shí)現(xiàn)“用戶注冊(cè)時(shí)過(guò)濾重復(fù)提交的消息”!
內(nèi)容:在前面幾篇文章中,我們介紹了Redis的數(shù)據(jù)結(jié)構(gòu)~列表List,簡(jiǎn)單介紹了其基本特性及其在實(shí)際項(xiàng)目中比較常見(jiàn)的、典型的應(yīng)用場(chǎng)景!從本文開(kāi)始,我們將著手介紹并實(shí)戰(zhàn)Redis的另外一種數(shù)據(jù)結(jié)構(gòu)~集合Set,介紹其基本的特性、在Dos環(huán)境下的命令行列表以及在Spring Boot2.0搭建的項(xiàng)目下實(shí)際應(yīng)用場(chǎng)景的代碼實(shí)戰(zhàn)等!
Redis的數(shù)據(jù)結(jié)構(gòu)-集合Set 跟 我們數(shù)學(xué)中的集合Set、JavaSE中的集合Set可以說(shuō)幾乎是相同的東西,,其特性均為: “無(wú)序”、“唯一”,即集合Set中存儲(chǔ)的元素是沒(méi)有順序且不重復(fù)的!
除此之外,其底層設(shè)計(jì)亦具有“異曲同工”之妙,即采用哈希表來(lái)實(shí)現(xiàn)的,故而其相應(yīng)的操作如添加、刪除、查找的復(fù)雜度都是 O(1) 。
一、DOS命令行的實(shí)操(基于redis-cli.exe工具即可實(shí)踐)
下面我們先采用 DOS下命令行的方式 來(lái)簡(jiǎn)單的認(rèn)識(shí)并實(shí)踐集合Set的相關(guān)命令,包括其常見(jiàn)的操作命令和“數(shù)學(xué)層面”集合的操作命令,如下圖所示:
(1)常見(jiàn)的操作命令無(wú)非就是“新增”、“查詢-獲取集合中的元素列表”、“查詢-獲取集合中的成員數(shù)目”、“查詢-獲取集合中隨機(jī)個(gè)數(shù)的元素列表”、“查詢-判斷某個(gè)元素是否為集合中的成員”、“刪除-移除集合中的元素”等。
下面我們貼出幾個(gè)比較典型、常見(jiàn)的操作命令所對(duì)應(yīng)的實(shí)際操作吧,其中相應(yīng)命令的含義各位小伙伴可以對(duì)照著上面那張圖進(jìn)行查看!
127.0.0.1:6379> SADD classOneStudents jacky xiaoming debug michael white
(integer) 5
127.0.0.1:6379> SMEMBERS classOneStudents
1) "jacky"
2) "michael"
3) "debug"
4) "xiaoming"
5) "white"
127.0.0.1:6379> SCARD classOneStudents
(integer) 5
127.0.0.1:6379> SADD classTwoStudents jacky xiaohong mary
(integer) 3
127.0.0.1:6379> SISMEMBER jacky classOneStudents
(integer) 0
127.0.0.1:6379> SISMEMBER classOneStudents jacky
(integer) 1
127.0.0.1:6379> SPOP classOneStudents
"white"
127.0.0.1:6379> SMEMBERS classOneStudents
1) "debug"
2) "jacky"
3) "xiaoming"
4) "michael"
127.0.0.1:6379> SRANDMEMBER classOneStudents 1
1) "jacky"
127.0.0.1:6379> SRANDMEMBER classOneStudents 3
1) "michael"
2) "xiaoming"
3) "debug"
127.0.0.1:6379> SRANDMEMBER classOneStudents 10
1) "jacky"
2) "michael"
3) "xiaoming"
4) "debug"
(2)而“數(shù)學(xué)層面”集合的操作命令則比較有意思,在這里我們主要介紹“交集”、“差集”和“并集”這三個(gè)操作命令,如下圖所示:
同樣的道理,我們依舊貼出這幾個(gè)操作命令所對(duì)應(yīng)的DOS操作,相應(yīng)命令的含義各位小伙伴可以對(duì)照著上面那張圖進(jìn)行查看!
127.0.0.1:6379> SDIFF classOneStudents classTwoStudents
1) "white"
2) "xiaoming"
3) "debug"
4) "michael"
127.0.0.1:6379> SDIFF classTwoStudents classOneStudents
1) "xiaohong"
2) "mary"
127.0.0.1:6379> SINTER classOneStudents classTwoStudents
1) "jacky"
127.0.0.1:6379> SUNION classOneStudents classTwoStudents
1) "debug"
2) "jacky"
3) "xiaohong"
4) "xiaoming"
5) "michael"
6) "mary"
二、集合Set命令對(duì)應(yīng)的代碼操作
基于這些操作命令,下面我們基于Spring Boot2.0搭建的項(xiàng)目,以“Java單元測(cè)試”的方式先進(jìn)行一波“代碼實(shí)戰(zhàn)”,將“Dos下的命令行操作”轉(zhuǎn)化為實(shí)際的代碼操作,如下所示:
@Test
public void method3() {
log.info("----開(kāi)始集合Set測(cè)試");
final String key1 = "SpringBootRedis:Set:10010";
final String key2 = "SpringBootRedis:Set:10011";
redisTemplate.delete(key1);
redisTemplate.delete(key2);
SetOperations<String, String> setOperations = redisTemplate.opsForSet();
setOperations.add(key1, new String[]{"a", "b", "c"});
setOperations.add(key2, new String[]{"b", "e", "f"});
log.info("---集合key1的元素:{}", setOperations.members(key1));
log.info("---集合key2的元素:{}", setOperations.members(key2));
log.info("---集合key1隨機(jī)取1個(gè)元素:{}", setOperations.randomMember(key1));
log.info("---集合key1隨機(jī)取n個(gè)元素:{}", setOperations.randomMembers(key1, 2L));
log.info("---集合key1元素個(gè)數(shù):{}", setOperations.size(key1));
log.info("---集合key2元素個(gè)數(shù):{}", setOperations.size(key2));
log.info("---元素a是否為集合key1的元素:{}", setOperations.isMember(key1, "a"));
log.info("---元素f是否為集合key1的元素:{}", setOperations.isMember(key1, "f"));
log.info("---集合key1和集合key2的差集元素:{}", setOperations.difference(key1, key2));
log.info("---集合key1和集合key2的交集元素:{}", setOperations.intersect(key1, key2));
log.info("---集合key1和集合key2的并集元素:{}", setOperations.union(key1, key2));
log.info("---從集合key1中彈出一個(gè)隨機(jī)的元素:{}", setOperations.pop(key1));
log.info("---集合key1的元素:{}", setOperations.members(key1));
log.info("---將c從集合key1的元素列表中移除:{}", setOperations.remove(key1, "c"));
}
點(diǎn)擊該單元測(cè)試方法左邊的“運(yùn)行”按鈕圖標(biāo),即可將該單元測(cè)試方式運(yùn)行起來(lái),其運(yùn)行后的結(jié)果如下圖所示:
相應(yīng)的api就不一一介紹了,其方法名可以說(shuō)是見(jiàn)名知意,大伙兒也可以照著擼一擼,敲一敲,實(shí)踐過(guò)后就會(huì)發(fā)現(xiàn)其實(shí)也沒(méi)那么復(fù)雜!
三、典型應(yīng)用場(chǎng)景實(shí)戰(zhàn)之~用戶注冊(cè)時(shí)過(guò)濾重復(fù)提交的信息
下面我們以實(shí)際項(xiàng)目開(kāi)發(fā)中典型的應(yīng)用場(chǎng)景為案例,以實(shí)際的代碼踐行集合Set各種重要的特性,即主要有“唯一性”、“無(wú)序性”。
我們首先以“集合Set中的元素具有唯一性”進(jìn)行開(kāi)刀,以“用戶注冊(cè)時(shí)過(guò)濾重復(fù)提交的信息”為案例進(jìn)行代碼實(shí)戰(zhàn)。
說(shuō)實(shí)在的,“重復(fù)提交”的業(yè)務(wù)場(chǎng)景在實(shí)際的項(xiàng)目開(kāi)發(fā)中其實(shí)并不少見(jiàn),比如用戶在前端提交信息時(shí)重復(fù)點(diǎn)擊按鈕多次,如果此時(shí)不采取相應(yīng)的限制措施,那么很有可能會(huì)在數(shù)據(jù)庫(kù)表中出現(xiàn)多條相同的數(shù)據(jù)條目!下面我們以“用戶注冊(cè)時(shí)重復(fù)提交信息”為案例進(jìn)行代碼實(shí)戰(zhàn)。
(1)工欲善其事,必先利其器,我們首先先在數(shù)據(jù)庫(kù)建立“用戶信息表user”,其DDL如下所示:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '姓名',
`email` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '郵箱',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_email` (`email`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表';
然后利用mybatis的代碼生成器或者逆向工程生成該數(shù)據(jù)庫(kù)表user的Entity實(shí)體信息、Mapper操作接口列表以及用于操作動(dòng)態(tài)Sql的Mapper.xml,在這里我就不貼出來(lái)其對(duì)應(yīng)源碼了,各位小伙伴可以前往文末提供的地址進(jìn)行下載查看!
(2)接下來(lái),我們建立一個(gè)Controller,并在其中開(kāi)發(fā)相應(yīng)的請(qǐng)求方法,用于處理前端用戶提交過(guò)來(lái)的“注冊(cè)信息”,其源碼如下所示:
/**
* 數(shù)據(jù)類型為Set - 數(shù)據(jù)元素不重復(fù)(過(guò)濾掉重復(fù)的元素;判斷一個(gè)元素是否存在于一個(gè)大集合中)
* @Author:debug (SteadyJack) – wx:debug0868
**/
@RestController
@RequestMapping("set")
public class SetController extends AbstractController {
@Autowired
private SetService setService;
//TODO:提交用戶注冊(cè)
@RequestMapping(value = "put",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse put(@RequestBody @Validated User user, BindingResult result){
String checkRes=ValidatorUtil.checkResult(result);
if (StrUtil.isNotBlank(checkRes)){
return new BaseResponse(StatusCode.Fail.getCode(),checkRes);
}
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
log.info("----用戶注冊(cè)信息:{}",user);
response.setData(setService.registerUser(user));
}catch (Exception e){
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
}
(3)其Service的處理邏輯如下所示:
/**
* 集合set服務(wù)處理邏輯
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260
**/
@Service
public class SetService {
private static final Logger log= LoggerFactory.getLogger(SetService.class);
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
//TODO:用戶注冊(cè)
@Transactional(rollbackFor = Exception.class)
public Integer registerUser(User user) throws Exception{
if (this.exist(user.getEmail())){
throw new RuntimeException(StatusCode.UserEmailHasExist.getMsg());
}
int res=userMapper.insertSelective(user);
if (res>0){
SetOperations<String,String> setOperations=redisTemplate.opsForSet();
setOperations.add(Constant.RedisSetKey,user.getEmail());
}
return user.getId();
}
//TODO:判斷郵箱是否已存在于緩存中
private Boolean exist(final String email) throws Exception{
//TODO:寫法二
SetOperations<String,String> setOperations=redisTemplate.opsForSet();
Long size=setOperations.size(Constant.RedisSetKey);
if (size>0 && setOperations.isMember(Constant.RedisSetKey,email)){
return true;
}else{
User user=userMapper.selectByEmail(email);
if (user!=null){
setOperations.add(Constant.RedisSetKey,user.getEmail());
return true;
}else{
return false;
}
}
}
從該代碼中我們可以看出,在插入用戶信息進(jìn)入數(shù)據(jù)庫(kù)之前,我們需要判斷該用戶是否存在于緩存集合Set中,如果已經(jīng)存在,則告知前端該“用戶郵箱”已經(jīng)存在(在這里我們認(rèn)為用戶的郵箱是唯一的,當(dāng)然啦,你可以調(diào)整為“用戶名”唯一…),如果緩存集合Set中不存在該郵箱,則插入數(shù)據(jù)庫(kù)中,并在“插入數(shù)據(jù)庫(kù)表成功” 之后,將該用戶郵箱塞到緩存集合Set中去即可。
值得一提的是,我們?cè)凇芭袛嗑彺鍿et中是否已經(jīng)存在該郵箱”的邏輯中,是先判斷緩存中是否存在,如果不存在,為了保險(xiǎn),我們會(huì)再去數(shù)據(jù)庫(kù)查詢郵箱是否真的不存在,如果真的是不存在,則將其“第一次”添加進(jìn)緩存Set中(這樣子可以在某種程度避免前端在重復(fù)點(diǎn)擊提交按鈕時(shí),產(chǎn)生瞬時(shí)高并發(fā)的現(xiàn)象,從而降低并發(fā)安全的風(fēng)險(xiǎn))!
當(dāng)然啦,這種寫法還是會(huì)存在一定的問(wèn)題的:即如果在插入數(shù)據(jù)庫(kù)時(shí)“掉鏈子”了,即發(fā)生異常了導(dǎo)致沒(méi)有插進(jìn)去,但是這個(gè)時(shí)候我們?cè)凇芭袛嗑彺婕蟂et中是否存在該郵箱時(shí)已經(jīng)將該郵箱添加進(jìn)緩存中一次了”,故而該郵箱將永遠(yuǎn)不能注冊(cè)了(但是實(shí)際上該郵箱并沒(méi)有真正插入到數(shù)據(jù)庫(kù)中哦?。?br>
(4)既然出現(xiàn)了問(wèn)題,那么就得先辦法去解決,如下代碼所示,為我們改造后的用戶注冊(cè)的服務(wù)邏輯:
@Transactional(rollbackFor = Exception.class)
public Integer registerUser(User user) throws Exception{
if (this.exist(user.getEmail())){
throw new RuntimeException(StatusCode.UserEmailHasExist.getMsg());
}
int res=0;
try{
res=userMapper.insertSelective(user);
if (res>0){
redisTemplate.opsForSet().add(Constant.RedisSetKey,user.getEmail());
}
}catch (Exception e){
throw e;
}finally {
//TODO:如果res不大于0,即代表插入到數(shù)據(jù)庫(kù)發(fā)生了異常,
//TODO:這個(gè)時(shí)候得將緩存Set中該郵箱移除掉
//TODO:因?yàn)樵谂袛嗍欠翊嬖跁r(shí) 加入了一次,不移除掉的話,就永遠(yuǎn)注冊(cè)不了該郵箱了
if (res<=0){
redisTemplate.opsForSet().remove(Constant.RedisSetKey,user.getEmail());
}
}
return user.getId();
}
從該服務(wù)處理邏輯中,我們可以得知主要使用集合Set的API方法包括:“插入”、“判斷是否為集合中的元素”、“集合中元素的個(gè)數(shù)”、“移除集合中指定的元素”等等
最后,我們打開(kāi)Postman對(duì)該接口進(jìn)行一番測(cè)試,如下幾張圖所示即可看到其最終的測(cè)試效果:
好了,本篇文章我們就介紹到這里了,建議各位小伙伴一定要照著文章提供的樣例代碼擼一擼,只有擼過(guò)才能知道這玩意是咋用的,否則就成了“空談?wù)摺保?/span>
對(duì)Redis相關(guān)技術(shù)棧以及實(shí)際應(yīng)用場(chǎng)景實(shí)戰(zhàn)感興趣的小伙伴可以前往Debug搭建的技術(shù)社區(qū)的課程中心進(jìn)行學(xué)習(xí)觀看:https://www.fightjava.com/web/index/course/detail/12 !
其他相關(guān)的技術(shù),感興趣的小伙伴可以關(guān)注底部Debug的技術(shù)公眾號(hào),或者加Debug的微信,拉你進(jìn)“微信版”的真正技術(shù)交流群!一起學(xué)習(xí)、共同成長(zhǎng)!
補(bǔ)充:
1、本文涉及到的相關(guān)的源代碼可以到此地址,check出來(lái)進(jìn)行查看學(xué)習(xí):
https://gitee.com/steadyjack/SpringBootRedis
2、目前Debug已將本文所涉及的內(nèi)容整理錄制成視頻教程,感興趣的小伙伴可以前往觀看學(xué)習(xí):https://www.fightjava.com/web/index/course/detail/12
3、關(guān)注一下Debug的技術(shù)微信公眾號(hào),最新的技術(shù)文章、課程以及技術(shù)專欄將會(huì)第一時(shí)間在公眾號(hào)發(fā)布哦!