Redis核心数据结构及其应用场景
首先附上redis官方文档: link.
String的应用场景
redis缓存String信息可分为以下几点:
1.) 单值缓存
- SET key value //键值对
2.) 批量缓存
- MSET key value [key value ...]
3.) 对象缓存 {"name":"zhangsan","height":"180cm"}
- SET user:Id value(json)
- MSET user:Id:name zhangsan user:Id:height 180cm
- MGET user:Id:name user:Id:height //获取对象信息
4.) 分布式锁
- SETNX biz:Id lock // 返回1 代表成功,只有第一次对key设值才会成功
–
5.) 计数器
![点赞](https://i-blog.csdnimg.cn/blog_migrate/ef8b1ea024f0876aa6db68a2d45b3c44.png)
使用redis的计数器实现点赞功能:
INCR pyq:likeCount:{msgid}
取消手滑点赞:
DECR pyq:likeCount:{msgid}
2:0>INCR pyq:likeCount:111
"2"
2:0>INCR pyq:likeCount:111
"3"
2:0>INCR pyq:likeCount:111
"4"
2:0>DE2:0>CR pyq:likeCount:111
"3"
6.) 分布式序列号
假设此时我们的数据库已经分表,此时再使用数据库的自增Id显然不对。在不考虑uuid,且主键类型为数字的情况
下,假设不使用雪花算法。
INCRBY fbs_Id 1000 // 一次拉取一千个id,返回自增结果
各个系统拉取一千个Id供本地使用 可以有效解决分布式情况下的Id自增
HashMap的应用场景
HSET key field value //存储一个哈希表key的键值
HSETNX key field value //存储一个不存在的哈希表key的键值
HMSET key field value [field value ...] //在一个哈希表key中存储多个键值对
HGET key field //获取哈希表key对应的field键值
HMGET key field [field ...] //批量获取哈希表key中多个field键值
HDEL key field [field ...] //删除哈希表key中的field键值
HLEN key //返回哈希表key中field的数量
HGETALL key //返回哈希表key中所有的键值
以购物车为例,使用hashSet实现这一功能。
1)以用户id为key
2)商品id为field
3)商品数量为value
hset cart:1001 10088 1 // 将id为10088的商品添加至user(1001)的购物车中
hincrby cart:1001 10088 1 // user(1001)还想再买一个
hdel cart:1001 10088 // 1001删除了该商品
hlen cart:1001 //获取商品总数
hgetall cart:1001 // 全选 咬咬牙买了
List的应用场景
LPUSH key value [value ...] //将一个或多个值value插入到key列表的表头(最左边)
RPUSH key value [value ...] //将一个或多个值value插入到key列表的表尾(最右边)
LPOP key //移除并返回key列表的头元素
RPOP key //移除并返回key列表的尾元素
BLPOP key [key ...] timeout // 从左侧弹出一个元素,如果size==0 等待timeout秒。
如果timeout==0 一直等待
常用数据结构(分布式)
Stack(栈) = LPUSH + LPOP
Queue(队列)= LPUSH + RPOP
Blocking MQ(阻塞队列)= LPUSH + BRPOP
爬虫分布式使用redis队列案例:
分布式抓取1688店铺数据:
1688中词大概有接近5w个,每个词需要爬取三页,每一页需要分批请求三次。
这种场景下,单线程是真的不够看的,在多线程的情况下,考虑词是趋于无限增大的情况下,抓取时间也是相应增大。
在这种情况下,考虑服务器横向扩容。n个服务器从redis队列中pop数据,保证各个服务器抓取词不重复。这边还涉及
redis发布/订阅功能。所有服务器统一订阅一个"频道",同一时间爬取。
@Override
public void run() {
while (!toStop) {
try {
running = false;
spider.tryFinish(); // 如果队列==0 尝试终止
final String url = spider.getRunData().getUrl(); // 分布式队列 pop
if (url == null) continue;
running = true;
final RunConfig runConfig = spider.getRunConfig();
final PageRequest pageRequest = makePageRequest(url);
IpPool.Ip ip = null;
if (runConfig.isProxy()) {
// ip 代理
ip = IpPool.proxyIp();
}
spider.getExecutorBiz().run();
} catch (Throwable e) {
// 失败放回队列中
// 其他的业务逻辑
} catch (Throwable e) {
e.printStackTrace();
}
}
}
private ExecutorService crawlers = Executors.newCachedThreadPool(); // 爬虫线程池
private List<SpiderExecutorThread> crawlerThreads = new CopyOnWriteArrayList<SpiderExecutorThread>(); // 爬虫线程引用镜像
/**
* 尝试终止
*/
public void tryFinish() {
boolean isRunning = false;
for (SpiderExecutorThread crawlerThread : crawlerThreads) {
if (crawlerThread.isRunning()) {
isRunning = true;
break;
}
}
synchronized (object){
boolean isEnd = runData.getUrlNum() == 0 && !isRunning;
if (isEnd) {
SpiderConfig.getSpiderConfig().setRunning(false);
stop();
}
}
}
/**
* 终止
*/
public void stop() {
for (SpiderExecutorThread crawlerThread : crawlerThreads) {
crawlerThread.toStop();
}
crawlers.shutdownNow();
log.info("spider finish....");
}
@Component
public class Redis1688RunData extends RunData {
@Autowired
private RedisTemplate redisTemplate;
/**
* 新增一个待采集的URL,接口需要做URL去重,爬虫线程将会获取到并进行处理;
*
* @param link
*/
@Override
public boolean addUrl(String link) {
redisTemplate.opsForList().leftPush("unCrawlKey", link);
return true;
}
/**
* 获取一个待采集的URL,并且将它从"待采集URL池"中移除
*/
@Override
public String getUrl() {
String link = (String) redisTemplate.opsForList().rightPop("unCrawlKey");
return link;
}
/**
* 获取待采集URL数量;
*/
@Override
public int getUrlNum() {
Long unVisitedUrl = redisTemplate.opsForList().size("unCrawlKey");
return Math.toIntExact(unVisitedUrl);
}
}
Set常用操作
SADD key member [member ...] //往集合key中存入元素,元素存在则忽略, 若key不存在则新建
SREM key member [member ...] //从集合key中删除元素
SMEMBERS key //获取集合key中所有元素
SCARD key //获取集合key的元素个数
SISMEMBER key member //判断member元素是否存在于集合key中
SRANDMEMBER key [count] //从集合key中选出count个元素,元素不从key中删除
SPOP key [count] //从集合key中选出count个元素,元素从key中删除
Set运算操作
SINTER key [key ...] //交集运算
SINTERSTORE destination key [key ..] //将交集结果存入新集合destination中
SUNION key [key ..] //并集运算
SUNIONSTORE destination key [key ...] //将并集结果存入新集合destination中
SDIFF key [key ...] //差集运算
SDIFFSTORE destination key [key ...] //将差集结果存入新集合destination中
使用场景 店铺案例:
以店铺案例为例,一个客户详情页需要各种json大对象信息组合而成,使用mysql进行缓存持久化,而且需要支持
各种搜索。搜索本客户自带属性或是不包含json对象内属性的搜索,mysql是非常简单可以做到的。表结构如下。
为了考虑索引失效的情况下,拉出大数据,单独建表。
–
客户表—建索引
详情页键入客户id
此时可以发现这边搜索用mysql索引是十分方便的,除了第一个宝贝关键词搜索。
宝贝关键词需要解释一下,他是拉取detail中的keyword_rank中json的1688关键词作为搜索。
一个客户可以有多个关键词,一个关键词也可以对应多个客户。典型的多对多结构。
如果使用mysql搜索,需要建一张第三方表,关键词对照表。为了这一个关键词改变表结构,
实在是不值得。redis set 可以十分简单的解决这一难题。
将关键词排名信息缓存到mysql时:
1.) SADD sell_keyword:word companyId //key 关键词 value Set<Id>
2.) SADD SELL_KEY_SET 关键词 // 为了做关键词模糊搜索
// 模糊搜索 所有相关的word
final Set<String> keywordScanSet = keywordScan(keyword.trim(), 1000);
Set<String> unionSet = new HashSet<>();
keywordScanSet.forEach(c -> unionSet.add(SELL_KEY.concat(c)));
final Set<String> idSet = redisTemplate.opsForSet().union("", unionSet);
// 取其并集 获取到Set<id> 使用sql in id
if (!idSet.isEmpty()) {
CriteriaBuilder.In<Long> in = cb.in(root.get("id"));
idSet.forEach(c -> in.value(Long.valueOf(c)));
predicates.add(in);
} else {
predicates.add(cb.equal(root.get("id").as(Long.class), -1L));
}
@Override
public Set<String> keywordScan(String keyword, int count) {
if (StringUtils.isBlank(keyword)) return new HashSet<>();
final ScanOptions.ScanOptionsBuilder scanOptionsBuilder = ScanOptions.scanOptions();
scanOptionsBuilder.count(1000);
scanOptionsBuilder.match("*" + keyword + "*");
final Cursor sell_keyword_set = redisTemplate.opsForSet().scan("sell_keyword_set", scanOptionsBuilder.build());
final Set<String> keywordSet = new HashSet<>();
while (sell_keyword_set.hasNext()) {
keywordSet.add(sell_keyword_set.next().toString());
if (keywordSet.size() == count) return keywordSet;
}
return keywordSet;
}