小言_互联网的博客

Redis实战(5)-数据结构Set实战之过滤用户注册重复提交的信息

552人阅读  评论(0)

概述:本系列博文所涉及的相关内容来源于debug亲自录制的实战课程:缓存中间件Redis技术入门与应用场景实战(SpringBoot2.x + 抢红包系统设计与实战),感兴趣的小伙伴可以点击自行前往学习(毕竟以视频的形式来掌握技术 会更快!)     原文链接:Redis实战(5)-数据结构Set实战之过滤用户注册重复提交的信息  ,文章所属技术专栏:缓存中间件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)常见的操作命令无非就是“新增”、“查询-获取集合中的元素列表”、“查询-获取集合中的成员数目”、“查询-获取集合中随机个数的元素列表”、“查询-判断某个元素是否为集合中的成员”、“删除-移除集合中的元素”等。

下面我们贴出几个比较典型、常见的操作命令所对应的实际操作吧,其中相应命令的含义各位小伙伴可以对照着上面那张图进行查看!


  
  1. 127.0.0.1: 6379> SADD classOneStudents jacky xiaoming debug michael white
  2. (integer) 5
  3. 127.0.0.1:6379> SMEMBERS classOneStudents
  4. 1) "jacky"
  5. 2) "michael"
  6. 3) "debug"
  7. 4) "xiaoming"
  8. 5) "white"
  9. 127.0.0.1:6379> SCARD classOneStudents
  10. (integer) 5
  11. 127.0.0.1:6379> SADD classTwoStudents jacky xiaohong mary
  12. (integer) 3
  13. 127.0.0.1:6379> SISMEMBER jacky classOneStudents
  14. (integer) 0
  15. 127.0.0.1:6379> SISMEMBER classOneStudents jacky
  16. (integer) 1
  17. 127.0.0.1:6379> SPOP classOneStudents
  18. "white"
  19. 127.0.0.1:6379> SMEMBERS classOneStudents
  20. 1) "debug"
  21. 2) "jacky"
  22. 3) "xiaoming"
  23. 4) "michael"
  24. 127.0.0.1:6379> SRANDMEMBER classOneStudents 1
  25. 1) "jacky"
  26. 127.0.0.1:6379> SRANDMEMBER classOneStudents 3
  27. 1) "michael"
  28. 2) "xiaoming"
  29. 3) "debug"
  30. 127.0.0.1:6379> SRANDMEMBER classOneStudents 10
  31. 1) "jacky"
  32. 2) "michael"
  33. 3) "xiaoming"
  34. 4) "debug"

(2)而“数学层面”集合的操作命令则比较有意思,在这里我们主要介绍“交集”、“差集”和“并集”这三个操作命令,如下图所示:

同样的道理,我们依旧贴出这几个操作命令所对应的DOS操作,相应命令的含义各位小伙伴可以对照着上面那张图进行查看!


  
  1. 127.0.0.1: 6379> SDIFF classOneStudents classTwoStudents
  2. 1) "white"
  3. 2) "xiaoming"
  4. 3) "debug"
  5. 4) "michael"
  6. 127.0.0.1: 6379> SDIFF classTwoStudents classOneStudents
  7. 1) "xiaohong"
  8. 2) "mary"
  9. 127.0.0.1: 6379> SINTER classOneStudents classTwoStudents
  10. 1) "jacky"
  11. 127.0.0.1: 6379> SUNION classOneStudents classTwoStudents
  12. 1) "debug"
  13. 2) "jacky"
  14. 3) "xiaohong"
  15. 4) "xiaoming"
  16. 5) "michael"
  17. 6) "mary"

二、集合Set命令对应的代码操作

基于这些操作命令,下面我们基于Spring Boot2.0搭建的项目,以“Java单元测试”的方式先进行一波“代码实战”,将“Dos下的命令行操作”转化为实际的代码操作,如下所示:


  
  1. @Test
  2. public void method3() {
  3. log.info( "----开始集合Set测试");
  4. final String key1 = "SpringBootRedis:Set:10010";
  5. final String key2 = "SpringBootRedis:Set:10011";
  6. redisTemplate.delete(key1);
  7. redisTemplate.delete(key2);
  8. SetOperations<String, String> setOperations = redisTemplate.opsForSet();
  9. setOperations.add(key1, new String[]{ "a", "b", "c"});
  10. setOperations.add(key2, new String[]{ "b", "e", "f"});
  11. log.info( "---集合key1的元素:{}", setOperations.members(key1));
  12. log.info( "---集合key2的元素:{}", setOperations.members(key2));
  13. log.info( "---集合key1随机取1个元素:{}", setOperations.randomMember(key1));
  14. log.info( "---集合key1随机取n个元素:{}", setOperations.randomMembers(key1, 2L));
  15. log.info( "---集合key1元素个数:{}", setOperations.size(key1));
  16. log.info( "---集合key2元素个数:{}", setOperations.size(key2));
  17. log.info( "---元素a是否为集合key1的元素:{}", setOperations.isMember(key1, "a"));
  18. log.info( "---元素f是否为集合key1的元素:{}", setOperations.isMember(key1, "f"));
  19. log.info( "---集合key1和集合key2的差集元素:{}", setOperations.difference(key1, key2));
  20. log.info( "---集合key1和集合key2的交集元素:{}", setOperations.intersect(key1, key2));
  21. log.info( "---集合key1和集合key2的并集元素:{}", setOperations.union(key1, key2));
  22. log.info( "---从集合key1中弹出一个随机的元素:{}", setOperations.pop(key1));
  23. log.info( "---集合key1的元素:{}", setOperations.members(key1));
  24. log.info( "---将c从集合key1的元素列表中移除:{}", setOperations.remove(key1, "c"));
  25. }

点击该单元测试方法左边的“运行”按钮图标,即可将该单元测试方式运行起来,其运行后的结果如下图所示:

相应的api就不一一介绍了,其方法名可以说是见名知意,大伙儿也可以照着撸一撸,敲一敲,实践过后就会发现其实也没那么复杂!

三、典型应用场景实战之~用户注册时过滤重复提交的信息

下面我们以实际项目开发中典型的应用场景为案例,以实际的代码践行集合Set各种重要的特性,即主要有“唯一性”、“无序性”。

我们首先以“集合Set中的元素具有唯一性”进行开刀,以“用户注册时过滤重复提交的信息”为案例进行代码实战。

说实在的,“重复提交”的业务场景在实际的项目开发中其实并不少见,比如用户在前端提交信息时重复点击按钮多次,如果此时不采取相应的限制措施,那么很有可能会在数据库表中出现多条相同的数据条目!下面我们以“用户注册时重复提交信息”为案例进行代码实战。

(1)工欲善其事,必先利其器,我们首先先在数据库建立“用户信息表user”,其DDL如下所示:


  
  1. CREATE TABLE `user` (
  2. `id` int( 11) NOT NULL AUTO_INCREMENT,
  3. `name` varchar( 255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '姓名',
  4. `email` varchar( 100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '邮箱',
  5. PRIMARY KEY (`id`),
  6. UNIQUE KEY `idx_email` (`email`) USING BTREE
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT= '用户表';

然后利用mybatis的代码生成器或者逆向工程生成该数据库表user的Entity实体信息、Mapper操作接口列表以及用于操作动态Sql的Mapper.xml,在这里我就不贴出来其对应源码了,各位小伙伴可以前往文末提供的地址进行下载查看!

(2)接下来,我们建立一个Controller,并在其中开发相应的请求方法,用于处理前端用户提交过来的“注册信息”,其源码如下所示:


  
  1. /**
  2. * 数据类型为Set - 数据元素不重复(过滤掉重复的元素;判断一个元素是否存在于一个大集合中)
  3. * @Author:debug (SteadyJack) – wx:debug0868
  4. **/
  5. @RestController
  6. @RequestMapping( "set")
  7. public class SetController extends AbstractController {
  8. @Autowired
  9. private SetService setService;
  10. //TODO:提交用户注册
  11. @RequestMapping(value = "put",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
  12. public BaseResponse put(@RequestBody @Validated User user, BindingResult result){
  13. String checkRes=ValidatorUtil.checkResult(result);
  14. if (StrUtil.isNotBlank(checkRes)){
  15. return new BaseResponse(StatusCode.Fail.getCode(),checkRes);
  16. }
  17. BaseResponse response= new BaseResponse(StatusCode.Success);
  18. try {
  19. log.info( "----用户注册信息:{}",user);
  20. response.setData(setService.registerUser(user));
  21. } catch (Exception e){
  22. response= new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
  23. }
  24. return response;
  25. }
  26. }

(3)其Service的处理逻辑如下所示:  


  
  1. /**
  2. * 集合set服务处理逻辑
  3. * @Author:debug (SteadyJack)
  4. * @Link: weixin-> debug0868 qq-> 1948831260
  5. **/
  6. @Service
  7. public class SetService {
  8. private static final Logger log= LoggerFactory.getLogger(SetService.class);
  9. @Autowired
  10. private UserMapper userMapper;
  11. @Autowired
  12. private RedisTemplate redisTemplate;
  13. //TODO:用户注册
  14. @Transactional(rollbackFor = Exception.class)
  15. public Integer registerUser(User user) throws Exception{
  16. if ( this.exist(user.getEmail())){
  17. throw new RuntimeException(StatusCode.UserEmailHasExist.getMsg());
  18. }
  19. int res=userMapper.insertSelective(user);
  20. if (res> 0){
  21. SetOperations<String,String> setOperations=redisTemplate.opsForSet();
  22. setOperations.add(Constant.RedisSetKey,user.getEmail());
  23. }
  24. return user.getId();
  25. }
  26. //TODO:判断邮箱是否已存在于缓存中
  27. private Boolean exist(final String email) throws Exception{
  28. //TODO:写法二
  29. SetOperations<String,String> setOperations=redisTemplate.opsForSet();
  30. Long size=setOperations.size(Constant.RedisSetKey);
  31. if (size> 0 && setOperations.isMember(Constant.RedisSetKey,email)){
  32. return true;
  33. } else{
  34. User user=userMapper.selectByEmail(email);
  35. if (user!= null){
  36. setOperations.add(Constant.RedisSetKey,user.getEmail());
  37. return true;
  38. } else{
  39. return false;
  40. }
  41. }
  42. }

从该代码中我们可以看出,在插入用户信息进入数据库之前,我们需要判断该用户是否存在于缓存集合Set中,如果已经存在,则告知前端该“用户邮箱”已经存在(在这里我们认为用户的邮箱是唯一的,当然啦,你可以调整为“用户名”唯一…),如果缓存集合Set中不存在该邮箱,则插入数据库中,并在“插入数据库表成功” 之后,将该用户邮箱塞到缓存集合Set中去即可。

值得一提的是,我们在“判断缓存Set中是否已经存在该邮箱”的逻辑中,是先判断缓存中是否存在,如果不存在,为了保险,我们会再去数据库查询邮箱是否真的不存在,如果真的是不存在,则将其“第一次”添加进缓存Set中(这样子可以在某种程度避免前端在重复点击提交按钮时,产生瞬时高并发的现象,从而降低并发安全的风险)!

当然啦,这种写法还是会存在一定的问题的:即如果在插入数据库时“掉链子”了,即发生异常了导致没有插进去,但是这个时候我们在“判断缓存集合Set中是否存在该邮箱时已经将该邮箱添加进缓存中一次了”,故而该邮箱将永远不能注册了(但是实际上该邮箱并没有真正插入到数据库中哦!)

(4)既然出现了问题,那么就得先办法去解决,如下代码所示,为我们改造后的用户注册的服务逻辑:


  
  1. @Transactional(rollbackFor = Exception.class)
  2. public Integer registerUser(User user) throws Exception{
  3. if ( this.exist(user.getEmail())){
  4. throw new RuntimeException(StatusCode.UserEmailHasExist.getMsg());
  5. }
  6. int res= 0;
  7. try{
  8. res=userMapper.insertSelective(user);
  9. if (res> 0){
  10. redisTemplate.opsForSet().add(Constant.RedisSetKey,user.getEmail());
  11. }
  12. } catch (Exception e){
  13. throw e;
  14. } finally {
  15. //TODO:如果res不大于0,即代表插入到数据库发生了异常,
  16. //TODO:这个时候得将缓存Set中该邮箱移除掉
  17. //TODO:因为在判断是否存在时 加入了一次,不移除掉的话,就永远注册不了该邮箱了
  18. if (res<= 0){
  19. redisTemplate.opsForSet().remove(Constant.RedisSetKey,user.getEmail());
  20. }
  21. }
  22. return user.getId();
  23. }

从该服务处理逻辑中,我们可以得知主要使用集合Set的API方法包括:“插入”、“判断是否为集合中的元素”、“集合中元素的个数”、“移除集合中指定的元素”等等

最后,我们打开Postman对该接口进行一番测试,如下几张图所示即可看到其最终的测试效果:

好了,本篇文章我们就介绍到这里了,建议各位小伙伴一定要照着文章提供的样例代码撸一撸,只有撸过才能知道这玩意是咋用的,否则就成了“空谈者”!

对Redis相关技术栈以及实际应用场景实战感兴趣的小伙伴可以前往debug搭建的技术社区的课程中心进行学习观看:http://www.fightjava.com/web/index/course/detail/12 !

其他相关的技术,感兴趣的小伙伴可以关注底部debug的技术公众号,或者加debug的微信(debug0868),拉你进“微信版”的真正技术交流群!一起学习、共同成长!

补充:

1、本文涉及到的相关的源代码可以到此地址,check出来进行查看学习:

https://gitee.com/steadyjack/SpringBootRedis

2、目前debug已将本文所涉及的内容整理录制成视频教程,感兴趣的小伙伴可以前往观看学习:http://www.fightjava.com/web/index/course/detail/12

3、关注一下debug的技术微信公众号,最新的技术文章、课程以及技术专栏将会第一时间在公众号发布哦!


转载:https://blog.csdn.net/u013871100/article/details/104208083
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场