Redis實戰(5)-數據結構Set實戰之過濾用戶註冊重複提交的信息

概述:本系列博文所涉及的相關內容來源於debug親自錄製的實戰課程:緩存中間件Redis技術入門與應用場景實戰(SpringBoot2.x + 搶紅包系統設計與實戰),感興趣的小夥伴能夠點擊自行前往學習(畢竟以視頻的形式來掌握技術 會更快!) ,文章所屬專欄:緩存中間件Redis技術入門與實戰html

摘要:毫無疑問,集合Set一樣也是緩存中間件Redis中其中一個重要的數據結構,其內部存儲的元素/成員具備「惟一」、「隨機」等特性,在實際的項目開發中一樣具備至關普遍的應用場景。本文咱們將介紹並實戰一種比較典型的業務場景~「重複提交」,即如何利用集合Set的相關特性實現「用戶註冊時過濾重複提交的消息」!
前端

內容:在前面幾篇文章中,咱們介紹了Redis的數據結構~列表List,簡單介紹了其基本特性及其在實際項目中比較常見的、典型的應用場景!從本文開始,咱們將着手介紹並實戰Redis的另一種數據結構~集合Set,介紹其基本的特性、在Dos環境下的命令行列表以及在Spring Boot2.0搭建的項目下實際應用場景的代碼實戰等!java

Redis的數據結構-集合Set 跟 咱們數學中的集合Set、JavaSE中的集合Set能夠說幾乎是相同的東西,,其特性均爲: 「無序」、「惟一」,即集合Set中存儲的元素是沒有順序且不重複的!git

除此以外,其底層設計亦具備「殊途同歸」之妙,即採用哈希表來實現的,故而其相應的操做如添加、刪除、查找的複雜度都是 O(1) 。web


1、DOS命令行的實操(基於redis-cli.exe工具便可實踐)redis

下面咱們先採用 DOS下命令行的方式 來簡單的認識並實踐集合Set的相關命令,包括其常見的操做命令和「數學層面」集合的操做命令,以下圖所示:數據庫

(1)常見的操做命令無非就是「新增」、「查詢-獲取集合中的元素列表」、「查詢-獲取集合中的成員數目」、「查詢-獲取集合中隨機個數的元素列表」、「查詢-判斷某個元素是否爲集合中的成員」、「刪除-移除集合中的元素」等。api

下面咱們貼出幾個比較典型、常見的操做命令所對應的實際操做吧,其中相應命令的含義各位小夥伴能夠對照着上面那張圖進行查看!緩存

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"

2、集合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就不一一介紹了,其方法名能夠說是見名知意,大夥兒也能夠照着擼一擼,敲一敲,實踐事後就會發現其實也沒那麼複雜!


3、典型應用場景實戰之~用戶註冊時過濾重複提交的信息

下面咱們以實際項目開發中典型的應用場景爲案例,以實際的代碼踐行集合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 + 搶紅包系統設計與實戰)

補充:

一、本文涉及到的相關的源代碼能夠到此地址,check出來進行查看學習:https://gitee.com/steadyjack/SpringBootRedis

二、目前debug已將本文所涉及的內容整理錄製成視頻教程,感興趣的小夥伴能夠前往觀看學習:https://edu.51cto.com/course/20384.html

相關文章
相關標籤/搜索