Set集合简介
毫无疑问,集合Set同样也是缓存中间件Redis中其中一个重要的数据结构,其内部存储的元素成员具有“唯一”、“随机”等特性,在实际的项目开发中同样具有相当广泛的应用场景。本文我们将介绍并实战一种比较典型的业务场景:重复提交,即如何利用集合Set的相关特性实现“用户注册时过滤重复提交的消息”!
除此之外,其底层设计亦具有“异曲同工”之妙,即采用哈希表来实现的,故而其相应的操作如添加、删除、查找的复杂度都是 O(1) 。
Set集合特点
Redis的数据结构-集合Set 跟 我们数学中的集合Set、JavaSE中的集合Set可以说几乎是相同的东西,,其特性均为: “无序”、“唯一”,即集合Set中存储的元素是没有顺序且不重复的!
Set集合使用
DOS命令行
常用操作
常见的操作命令无非就是“新增”、“查询-获取集合中的元素列表”、“查询-获取集合中的成员数目”、“查询-获取集合中随机个数的元素列表”、“查询-判断某个元素是否为集合中的成员”、“删除-移除集合中的元素”等。
下面我们贴出几个比较典型、常见的操作命令所对应的实际操作吧,其中相应命令的含义各位小伙伴可以对照着上面那张图进行查看!
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"
集合操作
“数学层面”集合的操作命令则比较有意思,在这里我们主要介绍“交集”、“差集”和“并集”这三个操作命令,如下图所示:
同样的道理,我们依旧贴出这几个操作命令所对应的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命令对应的代码操作
@Test
public void method() {
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"));
}
点击该单元测试方法左边的“运行”按钮图标,即可将该单元测试方式运行起来,其运行后的结果如下图所示:
注:看的再多不如实操起来
Set集合实战
直接上Controller层代码:
/**
* 数据类型为Set - 数据元素不重复(过滤掉重复的元素;判断一个元素是否存在于一个大集合中)
**/
@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;
}
}
Service层代码:
/**
* 集合set服务处理逻辑
**/
@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中是否存在该邮箱时已经将该邮箱添加进缓存中一次了”,故而该邮箱将永远不能注册了(但是实际上该邮箱并没有真正插入到数据库中哦!)
改造后的代码:
@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对该接口进行一番测试,如下几张图所示即可看到其最终的测试效果:
- 建议各位小伙伴一定要照着文章提供的样例代码撸一撸,只有撸过才能知道这玩意是咋用的,否则就成了“空谈者”!