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

概述:本系列博文所涉及的相關內容來源於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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章