redis 的set类型 和 Java的set集合功能类似,
set的主要功能是求 并集,交集,差集
1、黑名单过滤器业务场景分析
淘宝的商品评价功能,不是任何人就能评价的,有一种职业就是差评师,差评师就是勒索敲诈商家,
这种差评师在淘宝里面就被设置了黑名单,即使购买了商品,也评价不了。
2、黑名单校验器的redis技术方案
黑名单过滤器除了针对上文说的淘宝评价,针对用户黑名单外,其实还有ip黑名单、设备黑名单等。
在高并发的情况下,通过数据库过滤明显不符合要求,一般的做法都是通过Redis来实现的。
那redis那种数据结构适合做这种黑名单的呢?
答案是:set
步骤1:先把数据库的数据同步到redis的set集合中。
步骤2:评价的时候验证是否为黑名单,通过sismember命令来实现。
3.实现:
1.提前先把数据刷新到redis缓存中
/**
* 提前先把数据刷新到redis缓存中
*/
@PostConstruct
public void init(){
log.info("启动初始化 ..........");
List blacklist=this.blacklist();
//this.redisTemplate.delete(Constants.BLACKLIST_KEY);
blacklist.forEach(t->this.redisTemplate.opsForSet().add(Constants.BLACKLIST_KEY,t));
}
/**
* 模拟100个黑名单
*/
public List blacklist() {
List list=new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(i);
}
return list;
}
```
2.编写黑名单校验器接口
/**
*编写黑名单校验器接口
* true=黑名单
* false=不是黑名单
*/
@GetMapping(value = "/isBlacklist")
public boolean isBlacklist(Integer userId) {
boolean bo=false;
try {
//到set集合中去校验是否黑名单,
bo = this.redisTemplate.opsForSet().isMember(Constants.BLACKLIST_KEY,userId);
log.info("查询结果:{}", bo);
} catch (Exception ex) {
//这里的异常,一般是redis瘫痪 ,或 redis网络timeout
log.error("exception:", ex);
//TODO 走DB查询
}
return bo;
}
```
京豆抽奖一般是采用redis的set集合来操作的,那为什么是set集合适用于抽奖呢?
2个原因:
1.set集合的特点是元素不重复 存放1个 5个 10个京豆 谢谢参与
2.set集合支持随机读取
具体的技术方案是采用set集合的srandmember命令来实现,随机返回set的一个元素
案例实战:
步骤1:奖品的初始化
由于set集合是不重复,故在奖品初始化的时候,要为每个奖品设置一个序列号。
```
/**
*提前先把数据刷新到redis缓存中。
*/
@PostConstruct
public void init(){
log.info("启动初始化..........");
boolean bo=this.redisTemplate.hasKey(Constants.PRIZE_KEY);
if(!bo){
List crowds=this.prize();
crowds.forEach(t->this.redisTemplate.opsForSet().add(Constants.PRIZE_KEY,t));
}
}
/**
*按一定的概率初始化奖品
*/
public List prize() {
List list=new ArrayList<>();
//10个京豆,概率10%
for (int i = 0; i < 10; i++) {
list.add("10-"+i);
}
//5个京豆,概率20%
for (int i = 0; i < 20; i++) {
list.add("5-"+i);
}
//1个京豆,概率60%
for (int i = 0; i < 60; i++) {
list.add("1-"+i);
}
//0个京豆,概率10%
for (int i = 0; i < 10; i++) {
list.add("0-"+i);
}
return list;
}
```
步骤2:抽奖
@GetMapping(value = "/prize")
public String prize() {
String result="";
try {
//随机取1次。
String object = (String)this.redisTemplate.opsForSet().randomMember(Constants.PRIZE_KEY);
if (!StringUtils.isEmpty(object)){
//截取序列号 例如10-1
int temp=object.indexOf('-');
int no=Integer.valueOf(object.substring(0,temp));
switch (no){
case 0:
result="谢谢参与";
break;
case 1:
result="获得1个京豆";
break;
case 5:
result="获得5个京豆";
break;
case 10:
result="获得10个京豆";
break;
default:
result="谢谢参与";
}
}
log.info("查询结果:{}", object);
} catch (Exception ex) {
log.error("exception:", ex);
}
return result;
}
```
思考一个问题:支付宝的抽奖 和 京东京豆的抽奖有什么区别????
1. 京豆抽奖:奖品是可以重复,例如抽5京豆可以再抽到5京豆,即京豆是无限量抽。
2. 支付宝抽奖: 奖品不能重复抽,例如1万人抽1台华为手机;再给大家举一个熟悉的例子:
例如公司年会,抽中奖品的人,下一轮就不能重复抽取,不然就会重复中奖。
技术方案和京东的京豆类似,但是不同的是
京东的京豆用了srandmember命令,即随机返回set的一个元素
支付宝的抽奖要用spop命令,即随机返回并删除set中一个元素
为什么呢?
因为支付宝的奖品有限,不能重复抽,故抽奖完后,必须从集合中剔除中奖的人。
再举个每个人都参与过的例子,年会抽奖,你公司1000人,年会抽奖3等奖500名100元,2等奖50名1000元,1等奖10名10000元,
在抽奖的设计中就必须把已中奖的人剔除,不然就会出现重复中奖的概率。
案例实战:SpringBoot+Redis 实现支付宝抽奖
这个方法的逻辑是:使用set存储用户id,当抽某个奖品时,将奖品数量传入setOperations.pop
中,然后set会随机抽出对应数量的用户,并将其删除
#####步骤1:初始化抽奖数据
```
/**
*提前先把数据刷新到redis缓存中。
*/
@PostConstruct
public void init(){
log.info("启动初始化..........");
boolean bo=this.redisTemplate.hasKey(Constants.PRIZE_KEY);
if(!bo){
List crowds=this.prize();
crowds.forEach(t->this.redisTemplate.opsForSet().add(Constants.PRIZE_KEY,t));
}
}
/**
* 模拟10个用户来抽奖 list存放的是用户id
* 例如支付宝参与抽奖,就把用户id加入set集合中
* 例如公司抽奖,把公司所有的员工,工号都加入到set集合中
*/
public List prize() {
List list=new ArrayList<>();
for(int i=1;i<=10;i++){
list.add(i);
}
return list;
}
```
#####步骤2:抽奖逻辑
```
@GetMapping(value = "/prize")
public List prize(int num) {
try {
SetOperations setOperations= this.redisTemplate.opsForSet();
//spop命令,即随机返回并删除set中一个元素
List objs = setOperations.pop(Constants.PRIZE_KEY,num);
log.info("查询结果:{}", objs);
return objs;
} catch (Exception ex) {
log.error("exception:", ex);
}
return null;
}
```
当然上述逻辑不适合大规模用户抽奖,也不适用于抽奖活动天数为几天甚至更长的场景。
抽奖活动天数较长,参考:使用redis zset实现抽奖,奖池商品按时间随机分布 - T~Z - 博客园
、
### 随机展示业务场景分析
思考题:为什么要随机展示?
因为展示的区域有限啊,在那么小的地方展示全部数据是不可能的,通常的做法就是随机展示一批数据,然后用户点击“换一换”按钮,再随机展示另一批。
### 随机展示的redis技术方案
上文已经说了随机展示的原因就是区域有限,而区域有限的地方通常就是首页或频道页,这些位置通常都是访问量并发量非常高的,
一般是不可能采用数据库来实现的,通常都是Redis来实现。
redis的实现技术方案:
步骤1:先把数据准备好,把所有需要展示的内容存入redis的Set数据结构中。
步骤2:通过srandmember命令随机拿一批数据出来。
### SpringBoot+Redis 实现高并发随机展示
#### SpringBoot+Redis 实现好友\QQ群 随机推荐
##### 步骤1:提前先把数据刷新到redis缓存中。
```
/**
*提前先把数据刷新到redis缓存中。
*/
@PostConstruct
public void init(){
log.info("启动初始化 群..........");
List crowds=this.crowd();
this.redisTemplate.delete(Constants.CROWD_KEY);
crowds.forEach(t->this.redisTemplate.opsForSet().add(Constants.CROWD_KEY,t));
}
/**
* 模拟100个热门群,用于推荐
*/
public List crowd() {
List list=new ArrayList<>();
for (int i = 0; i < 100; i++) {
Random rand = new Random();
int id= rand.nextInt(10000);
list.add("群"+id);
}
return list;
}
```
##### 步骤2:编写随机查询接口
```
@GetMapping(value = "/crowd")
public List crowd() {
List list=null;
try {
//采用redis set数据结构,随机取出10条数据
list = this.redisTemplate.opsForSet().randomMembers(Constants.CROWD_KEY,10);
log.info("查询结果:{}", list);
} catch (Exception ex) {
//这里的异常,一般是redis瘫痪 ,或 redis网络timeout
log.error("exception:", ex);
//TODO 走DB查询
}
return list;
}
```
微博榜单和QQ群的区别是:微博榜单是整块数据的,所以随机的数据要按块来推荐
所以我们要定义一个java bean来包装整块数据
##### 步骤1:提前先把数据刷新到redis缓存中。
数据类:
```
@Data
public class WeiboList {
private int id;
/**
* 榜单名称
*/
private String name;
private List users;
}
```
```
/**
* 模拟10个热门榜单,用于推荐
*/
public List list() {
List list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeiboList wl=new WeiboList();
wl.setId(i);
wl.setName("榜单"+i);
Random rand = new Random();
List users=new ArrayList<>();
for (int j=0;j<3;j++){
int id= rand.nextInt(10000);
users.add("user:"+id);
}
wl.setUsers(users);
list.add(wl);
}
return list;
}
```
#### 一、微博点赞业务场景分析
梳理点赞的业务场景,它有2个接口:
第一个:点赞或取消点赞,用户点击功能
第二个接口:查看帖子信息:通过用户id 和帖子id,查看该帖子的点赞数、该用户是否点赞状态。
#### 二、微博点赞的技术方案
点赞的关键技术就是要判断该用户是否点赞,已重复点赞的不允许再点赞,即过滤重复,虽然业务不复杂,可以采用数据库直接实现,
但是对于微博这种高并发的场景,不可能查数据库的,一般是缓存,即redis
ok,我们来对上文梳理的2个接口进行技术分析:
第一个:点赞或取消点赞,用户点击功能
采用的是redis的set数据结构,key=like:postid value={userid}
postid :帖子id
采用sadd命令,添加点赞
```
127.0.0.1:6379> sadd like:1000 101
(integer) 1
127.0.0.1:6379> sadd like:1000 102
(integer) 1
127.0.0.1:6379> sadd like:1000 103
(integer) 1
127.0.0.1:6379> smembers like:1000
1) "101"
2) "102"
3) "103"
```
采用srem命令,取消点赞
```
127.0.0.1:6379> srem like:1000 101
(integer) 1
127.0.0.1:6379> smembers like:1000
1) "102"
2) "103"
```
第二个接口:查看帖子信息:通过用户id 和帖子id,查看该帖子的点赞数、该用户是否点赞状态。
采用scard命令,点赞总数
```
127.0.0.1:6379> smembers like:1000
1) "102"
2) "103"
127.0.0.1:6379> scard like:1000
(integer) 2
```
采用sismember命令,判断是否点赞
```
127.0.0.1:6379> smembers like:1000
1) "102"
2) "103"
127.0.0.1:6379> sismember like:1000 102
(integer) 1
127.0.0.1:6379> sismember like:1000 101
(integer) 0
```
三、案例实战:SpringBoot+Redis 实现微博点赞
#####步骤1:点赞逻辑
```
/**
* 点赞
*/
@GetMapping(value = "/dolike")
public String dolike(int postid,int userid) {
String result="";
try {
String key=Constants.LIKE_KEY+postid;
long object = this.redisTemplate.opsForSet().add(key,userid);
if (object==1){
result="点赞成功";
}else{
result="你已重复点赞";
}
log.info("查询结果:{}", object);
} catch (Exception ex) {
log.error("exception:", ex);
}
return result;
}
/**
* 取消点赞
*/
@GetMapping(value = "/undolike")
public String undolike(int postid,int userid) {
String result="";
try {
String key=Constants.LIKE_KEY+postid;
long object = this.redisTemplate.opsForSet().remove(key,userid);
if (object==1){
result="取消成功";
}else{
result="你已重复取消点赞";
}
log.info("查询结果:{}", object);
} catch (Exception ex) {
log.error("exception:", ex);
}
return result;
}
/**
* 根据postid userid查看帖子信息,返回结果是点赞总数和是否点赞
*/
@GetMapping(value = "/getpost")
public Map getpost(int postid,int userid) {
Map map=new HashMap();
String result="";
try {
String key=Constants.LIKE_KEY+postid;
long size = this.redisTemplate.opsForSet().size(key);
boolean bo=this.redisTemplate.opsForSet().isMember(key,userid);
map.put("size",size);
map.put("isLike",bo);
log.info("查询结果:{}", map);
} catch (Exception ex) {
log.error("exception:", ex);
}
return map;
}
/**
* 查看点赞明细,就是有哪些人
*/
@GetMapping(value = "/likedetail")
public Set likedetail(int postid) {
Set set=null;
try {
String key=Constants.LIKE_KEY+postid;
set = this.redisTemplate.opsForSet().members(key);
log.info("查询结果:{}", set);
} catch (Exception ex) {
log.error("exception:", ex);
}
return set;
}
```
# 基于Redis的微博关注与粉丝
### 一、微博关注与粉丝的业务场景分析
阿甘关注了雷军:阿甘就是雷军的粉丝follower
雷军被阿甘关注:雷军就是阿甘的关注followee
### 二、微博关注与粉丝的redis技术方案
技术方案:每个用户都有2个集合:关注集合和粉丝集合
例如 阿甘关注了雷军,做了2个动作
1.把阿甘的userid加入雷军的粉丝follower集合set
2.把雷军的userid加入阿甘的关注followee集合set
集合key设计
阿甘的关注集合 key=followee:阿甘userid
雷军的粉丝集合 key=follower:雷军userid
### SpringBoot+Redis 实现微博关注与粉丝
/**
* 阿甘关注了雷军
*/
public void follow(Integer userId,Integer followeeId){
SetOperations opsForSet = redisTemplate.opsForSet();
//阿甘的关注集合
String followeekey = Constants.CACHE_KEY_FOLLOWEE + userId;
//把雷军的followeeId,加入阿甘的关注集合中
opsForSet.add(followeekey,followeeId);
//雷军的粉丝集合
String followerkey=Constants.CACHE_KEY_FOLLOWER+followeeId;
//把阿甘的userid加入雷军的粉丝follower集合set
opsForSet.add(followerkey,userId);
}
/**
* 查看我的粉丝
*/
public List myFollower(Integer userId){
SetOperations opsForSet = redisTemplate.opsForSet();
//粉丝集合
String followerkey=Constants.CACHE_KEY_FOLLOWER+userId;
Set sets= opsForSet.members(followerkey);
return this.getUserInfo(sets);
}
/**
* 查看我的关注
*/
public List myFollowee(Integer userId){
SetOperations opsForSet = redisTemplate.opsForSet();
//关注集合
String followeekey = Constants.CACHE_KEY_FOLLOWEE + userId;
Set sets= opsForSet.members(followeekey);
return this.getUserInfo(sets);
}
/**
* 求2个用户的关注交集
*/
public List intersect(Integer userId1,Integer userId2){
SetOperations opsForSet = redisTemplate.opsForSet();
String followeekey1 = Constants.CACHE_KEY_FOLLOWEE + userId1;
String followeekey2 = Constants.CACHE_KEY_FOLLOWEE + userId2;
//求2个集合的交集
Set sets= opsForSet.intersect(followeekey1,followeekey2);
return this.getUserInfo(sets);
}
/**
* 获取用户信息
*/
private List getUserInfo(Set set){
List list=new ArrayList<>();
List hashKeys=new ArrayList<>();
hashKeys.add("id");
hashKeys.add("username");
HashOperations opsForHash=redisTemplate.opsForHash();
for (Integer id:set) {
String hkey = Constants.CACHE_KEY_USER + id;
List
### 一、计算好友关系业务场景分析
共同关注:是计算出阿甘和雷军共同关注的人有哪些?
我关注的人也关注他:是计算出我阿甘关注的人群中,有哪些人同时和我一样关注了雷军
### 二、计算好友关系的redis技术方案
思考题:如果是采用数据库来实现用户的关系,一般SQL怎么写? 例如 阿甘关注10个人,雷军关注100个人,让你计算2人的共同关注那些人?
SQL的写法,一般是采用in 或 not in 来实现。但是对于互联网高并发的系统来说,in not in 明显不适合。
一般的做法是采用redis的set集合来实现。
Redis Set数据结构,非常适合存储好友、关注、粉丝、感兴趣的人的集合。然后采用set的命令就能得出我们想要的数据。
1. sinter命令:获得A和B两个用户的共同好友
2. sismember命令:判断C是否为B的好友
3. scard命令:获取好友数量
### SpringBoot+Redis 计算微博好友关系
目标:共同关注:是计算出阿甘和雷军共同关注的人有哪些?
```
@ApiOperation(value="求2个用户的关注交集")
@GetMapping(value = "/intersect")
public List intersect(Integer userId1, Integer userId2){
return this.relationService.intersect(userId1,userId2);
}
```
```
/**
* 求2个用户的关注交集
*/
public List intersect(Integer userId1,Integer userId2){
SetOperations opsForSet = redisTemplate.opsForSet();
String followeekey1 = Constants.CACHE_KEY_FOLLOWEE + userId1;
String followeekey2 = Constants.CACHE_KEY_FOLLOWEE + userId2;
//求2个集合的交集
Set sets= opsForSet.intersect(followeekey1,followeekey2);
return this.getUserInfo(sets);
}
UV(Unique Visitor)独立访客,统计1天内访问某站点的用户数(以cookie为依据);访问网站的一台电脑客户端为一个访客。可以理解成访问某网站的电脑的数量。网站判断来访电脑的身份是通过来访电脑的cookies实现的。如果更换了IP后但不清除cookies,再访问相同网站,该网站的统计中UV数是不变的。如果用户不保存cookies访问、清除了cookies或者更换设备访问,计数会加1。00:00-24:00内相同的客户端多次访问只计为1个访客。
UV、PV是对网站流量的统计,适用于计算机网站和触屏版网站。
APP对用户的判断标准不再是UV、PV,而是新增用户、活跃用户、启动次数。
参考:11. 优秀的基数统计算法--HyperLogLog - 古明地盆 - 博客园
### HyperLogLog 命令详解
HyperLogLog 目前只支持3个命令,PFADD、PFCOUNT、PFMERGE
#### PFADD
将元素加入到HyperLogLog数据结构中,如果 HyperLogLog 的基数估算值在命令执行之后出现了变化,那么命令返回1,否则返回0。
#### PFCOUNT
返回给定 HyperLogLog 的基数估算值。
```
127.0.0.1:6379> pfadd uv 192.168.1.100 192.168.1.101 192.168.1.102 192.168.1.103
(integer) 1
127.0.0.1:6379> pfadd uv 192.168.1.100
(integer) 0
127.0.0.1:6379> pfcount uv
(integer) 4
```
#### PFMERGE
把多个HyperLogLog合并为一个HyperLogLog,合并后的HyperLogLog的基数是通过所有的HyperLogLog进行并集后,得出来的。
```
127.0.0.1:6379> pfadd uv1 192.168.1.100 192.168.1.101 192.168.1.102 192.168.1.103
(integer) 1
127.0.0.1:6379> pfcount uv1
(integer) 4
127.0.0.1:6379> pfadd uv2 192.168.1.100 192.168.2.101 192.168.2.102 192.168.2.103
(integer) 1
127.0.0.1:6379> pfadd uv3 192.168.1.100 192.168.3.101 192.168.3.102 192.168.3.103
(integer) 1
127.0.0.1:6379> pfmerge aganuv uv1 uv2 uv3
OK
127.0.0.1:6379> pfcount aganuv
(integer) 10
```
这个命令很重要,例如你可以统计一周 或 一个月的uv 就可以使用此命令来轻易实现。
### 3.案例实战:基于Redis的UV计算
#### 步骤1:模拟UV访问
public class TaskService {
@Autowired
private RedisTemplate redisTemplate;
/**
*模拟UV访问
*/
@PostConstruct
public void init(){
log.info("模拟UV访问 ..........");
new Thread(()->this.refreshData()).start();
}
/**
* 刷新当天的统计数据
*/
public void refreshDay(){
Random rand = new Random();
String ip=rand.nextInt(999)+"."+
rand.nextInt(999)+"."+
rand.nextInt(999)+"."+
rand.nextInt(999);
//redis 命令 pfadd
long n=this.redisTemplate.opsForHyperLogLog().add(Constants.PV_KEY,ip);
log.debug("ip={} , returen={}",ip,n);
}
/**
* 按天模拟UV统计
*/
public void refreshData(){
while (true){
this.refreshDay();
//TODO 在分布式系统中,建议用xxljob来实现定时
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
```
#### 步骤2:实现UV统计功能
```
@RestController
@Slf4j
public class Controller {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping(value = "/uv")
public long uv() {
//redis命令 pfcount
long size=this.redisTemplate.opsForHyperLogLog().size(Constants.PV_KEY);
return size;
}
}
```
#### 步骤2:实现UV统计功能
@RestController
@Slf4j
public class Controller {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping(value = "/uv")
public long uv() {
//redis命令 pfcount
long size=this.redisTemplate.opsForHyperLogLog().size(Constants.PV_KEY);
return size;
}
}
```