概述:本系列博文所涉及的相關內容來源於debug親自錄製的實戰課程:緩存中間件Redis技術入門與應用場景實戰(SpringBoot2.x + 搶紅包系統設計與實戰),感興趣的小夥伴可以點擊自行前往學習(畢竟以視頻的形式來掌握技術 會更快!) ,文章所屬專欄:緩存中間件Redis技術入門與實戰
摘要:毫無疑問,集合Set同樣也是緩存中間件Redis中其中一個重要的數據結構,其內部存儲的元素/成員具有“唯一”、“隨機”等特性,在實際的項目開發中同樣具有相當廣泛的應用場景。本文我們將介紹並實戰一種比較典型的業務場景~“重複提交”,即如何利用集合Set的相關特性實現“用戶註冊時過濾重複提交的消息”!
內容:在前面幾篇文章中,我們介紹了Redis的數據結構~列表List,簡單介紹了其基本特性及其在實際項目中比較常見的、典型的應用場景!從本文開始,我們將着手介紹並實戰Redis的另外一種數據結構~集合Set,介紹其基本的特性、在Dos環境下的命令行列表以及在Spring Boot2.0搭建的項目下實際應用場景的代碼實戰等!
Redis的數據結構-集合Set 跟 我們數學中的集合Set、JavaSE中的集合Set可以說幾乎是相同的東西,,其特性均爲: “無序”、“唯一”,即集合Set中存儲的元素是沒有順序且不重複的!
除此之外,其底層設計亦具有“異曲同工”之妙,即採用哈希表來實現的,故而其相應的操作如添加、刪除、查找的複雜度都是 O(1) 。
一、DOS命令行的實操(基於redis-cli.exe工具即可實踐)
下面我們先採用 DOS下命令行的方式 來簡單的認識並實踐集合Set的相關命令,包括其常見的操作命令和“數學層面”集合的操作命令,如下圖所示:
(1)常見的操作命令無非就是“新增”、“查詢-獲取集合中的元素列表”、“查詢-獲取集合中的成員數目”、“查詢-獲取集合中隨機個數的元素列表”、“查詢-判斷某個元素是否爲集合中的成員”、“刪除-移除集合中的元素”等。
下面我們貼出幾個比較典型、常見的操作命令所對應的實際操作吧,其中相應命令的含義各位小夥伴可以對照着上面那張圖進行查看!
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)而“數學層面”集合的操作命令則比較有意思,在這裏我們主要介紹“交集”、“差集”和“並集”這三個操作命令,如下圖所示:
同樣的道理,我們依舊貼出這幾個操作命令所對應的DOS操作,相應命令的含義各位小夥伴可以對照着上面那張圖進行查看!
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命令對應的代碼操作
基於這些操作命令,下面我們基於Spring Boot2.0搭建的項目,以“Java單元測試”的方式先進行一波“代碼實戰”,將“Dos下的命令行操作”轉化爲實際的代碼操作,如下所示:
@Test public void method3() { log.info("----開始集合Set測試"); 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隨機取1個元素:{}", setOperations.randomMember(key1)); log.info("---集合key1隨機取n個元素:{}", setOperations.randomMembers(key1, 2L)); log.info("---集合key1元素個數:{}", setOperations.size(key1)); log.info("---集合key2元素個數:{}", 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中彈出一個隨機的元素:{}", setOperations.pop(key1)); log.info("---集合key1的元素:{}", setOperations.members(key1)); log.info("---將c從集合key1的元素列表中移除:{}", setOperations.remove(key1, "c")); }
點擊該單元測試方法左邊的“運行”按鈕圖標,即可將該單元測試方式運行起來,其運行後的結果如下圖所示:
相應的api就不一一介紹了,其方法名可以說是見名知意,大夥兒也可以照着擼一擼,敲一敲,實踐過後就會發現其實也沒那麼複雜!
三、典型應用場景實戰之~用戶註冊時過濾重複提交的信息
下面我們以實際項目開發中典型的應用場景爲案例,以實際的代碼踐行集合Set各種重要的特性,即主要有“唯一性”、“無序性”。
我們首先以“集合Set中的元素具有唯一性”進行開刀,以“用戶註冊時過濾重複提交的信息”爲案例進行代碼實戰。
說實在的,“重複提交”的業務場景在實際的項目開發中其實並不少見,比如用戶在前端提交信息時重複點擊按鈕多次,如果此時不採取相應的限制措施,那麼很有可能會在數據庫表中出現多條相同的數據條目!下面我們以“用戶註冊時重複提交信息”爲案例進行代碼實戰。
(1)工欲善其事,必先利其器,我們首先先在數據庫建立“用戶信息表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的代碼生成器或者逆向工程生成該數據庫表user的Entity實體信息、Mapper操作接口列表以及用於操作動態Sql的Mapper.xml,在這裏我就不貼出來其對應源碼了,各位小夥伴可以前往文末提供的地址進行下載查看!
(2)接下來,我們建立一個Controller,並在其中開發相應的請求方法,用於處理前端用戶提交過來的“註冊信息”,其源碼如下所示:
/** * 數據類型爲Set - 數據元素不重複(過濾掉重複的元素;判斷一個元素是否存在於一個大集合中) * @Author:debug (SteadyJack) – wx:debug0868 **/ @RestController @RequestMapping("set") public class SetController extends AbstractController { @Autowired private SetService setService; //TODO:提交用戶註冊 @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("----用戶註冊信息:{}",user); response.setData(setService.registerUser(user)); }catch (Exception e){ response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage()); } return response; } }
(3)其Service的處理邏輯如下所示:
/** * 集合set服務處理邏輯 * @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:用戶註冊 @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; } } }
從該代碼中我們可以看出,在插入用戶信息進入數據庫之前,我們需要判斷該用戶是否存在於緩存集合Set中,如果已經存在,則告知前端該“用戶郵箱”已經存在(在這裏我們認爲用戶的郵箱是唯一的,當然啦,你可以調整爲“用戶名”唯一…),如果緩存集合Set中不存在該郵箱,則插入數據庫中,並在“插入數據庫表成功” 之後,將該用戶郵箱塞到緩存集合Set中去即可。
值得一提的是,我們在“判斷緩存Set中是否已經存在該郵箱”的邏輯中,是先判斷緩存中是否存在,如果不存在,爲了保險,我們會再去數據庫查詢郵箱是否真的不存在,如果真的是不存在,則將其“第一次”添加進緩存Set中(這樣子可以在某種程度避免前端在重複點擊提交按鈕時,產生瞬時高併發的現象,從而降低併發安全的風險)!
當然啦,這種寫法還是會存在一定的問題的:即如果在插入數據庫時“掉鏈子”了,即發生異常了導致沒有插進去,但是這個時候我們在“判斷緩存集合Set中是否存在該郵箱時已經將該郵箱添加進緩存中一次了”,故而該郵箱將永遠不能註冊了(但是實際上該郵箱並沒有真正插入到數據庫中哦!)
(4)既然出現了問題,那麼就得先辦法去解決,如下代碼所示,爲我們改造後的用戶註冊的服務邏輯:
@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,即代表插入到數據庫發生了異常, //TODO:這個時候得將緩存Set中該郵箱移除掉 //TODO:因爲在判斷是否存在時 加入了一次,不移除掉的話,就永遠註冊不了該郵箱了 if (res<=0){ redisTemplate.opsForSet().remove(Constant.RedisSetKey,user.getEmail()); } } return user.getId(); }
從該服務處理邏輯中,我們可以得知主要使用集合Set的API方法包括:“插入”、“判斷是否爲集合中的元素”、“集合中元素的個數”、“移除集合中指定的元素”等等
最後,我們打開Postman對該接口進行一番測試,如下幾張圖所示即可看到其最終的測試效果:
好了,本篇文章我們就介紹到這裏了,建議各位小夥伴一定要照着文章提供的樣例代碼擼一擼,只有擼過才能知道這玩意是咋用的,否則就成了“空談者”!對Redis相關技術棧以及實際應用場景實戰感興趣的小夥伴可以咱們51cto學院 debug親自錄製的課程進行學習:緩存中間件Redis技術入門與應用場景實戰(SpringBoot2.x + 搶紅包系統設計與實戰)
補充:
1、本文涉及到的相關的源代碼可以到此地址,check出來進行查看學習:https://gitee.com/steadyjack/SpringBootRedis
2、目前debug已將本文所涉及的內容整理錄製成視頻教程,感興趣的小夥伴可以前往觀看學習:https://edu.51cto.com/course/20384.html