使用Redis实现UA池

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 最近忙于业务开发、交接和游戏,加上碰上了不定时出现的犹豫期和困惑期,荒废学业了一段时间。天冷了,要重新拾起开始下阶段的学习了。之前接触到的一些数据搜索项目,涉及到请求模拟,基于反爬需要使用随机的User Agent,于是使用Redis实现了一个十分简易的UA池。

微信截图_20220513113241.png


前提



最近忙于业务开发、交接和游戏,加上碰上了不定时出现的犹豫期和困惑期,荒废学业了一段时间。天冷了,要重新拾起开始下阶段的学习了。之前接触到的一些数据搜索项目,涉及到请求模拟,基于反爬需要使用随机的User Agent,于是使用Redis实现了一个十分简易的UA池。


背景



最近的一个需求,有模拟请求的逻辑,要求每次请求的请求头中的User Agent要满足下面几点:


  • 每次获取的User Agent是随机的。
  • 每次获取的User Agent(短时间内)不能重复。
  • 每次获取的User Agent必须带有主流的操作系统信息(可以是UinuxWindowsIOS和安卓等等)。


这里三点都可以从UA数据的来源解决,实际上我们应该关注具体的实现方案。简单分析一下,流程如下:


微信截图_20220513113249.png


在设计UA池的时候,它的数据结构和环形队列十分类似:


微信截图_20220513113256.png



上图中,假设不同颜色的UA是完全不同的UA,它们通过洗牌算法打散放进去环形队列中,实际上每次取出一个UA之后,只需要把游标cursor前进或者后退一格即可(甚至可以把游标设置到队列中的任意元素)。最终的实现就是:需要通过中间件实现分布式队列(只是队列,不是消息队列)。


具体实现方案



毫无疑问需要一个分布式数据库类型的中间件才能存放已经准备好的UA,第一印象就感觉Redis会比较合适。接下来需要选用Redis的数据类型,主要考虑几个方面:


  • 具备队列性质。
  • 最好支持随机访问。
  • 元素入队、出队和随机访问的时间复杂度要低,毕竟获取UA的接口访问量会比较大。


支持这几个方面的Redis数据类型就是List,不过注意List本身不能去重,去重的工作可以用代码逻辑实现。然后可以想象客户端获取UA的流程大致如下:


微信截图_20220513113303.png


结合前面的分析,编码过程有如下几步:


  1. 准备好需要导入的UA数据,可以从数据源读取,也可以直接文件读取。
  2. 因为需要导入的UA数据集合一般不会太大,考虑先把这个集合的数据随机打散,如果使用Java开发可以直接使用Collections#shuffle()洗牌算法,当然也可以自行实现这个数据随机分布的算法,这一步对于一些被模拟方会严格检验UA合法性的场景是必须的
  3. 导入UA数据到Redis列表中。
  4. 编写RPOP + LPUSHLua脚本,实现分布式循环队列。


编码和测试示例



引入Redis的高级客户端Lettuce依赖:


<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>
复制代码


编写RPOP + LPUSHLua脚本,Lua脚本名字暂称为L_RPOP_LPUSH.lua,放在resources/scripts/lua目录下:


local key = KEYS[1]
local value = redis.call('RPOP', key)
redis.call('LPUSH', key, value)
return value
复制代码


这个脚本十分简单,但是已经实现了循环队列的功能。剩下来的测试代码如下:


public class UaPoolTest {
    private static RedisCommands<String, String> COMMANDS;
    private static AtomicReference<String> LUA_SHA = new AtomicReference<>();
    private static final String KEY = "UA_POOL";
    @BeforeClass
    public static void beforeClass() throws Exception {
        // 初始化Redis客户端
        RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build();
        RedisClient redisClient = RedisClient.create(uri);
        StatefulRedisConnection<String, String> connect = redisClient.connect();
        COMMANDS = connect.sync();
        // 模拟构建UA池的原始数据,假设有10个UA,分别是UA-0 ... UA-9
        List<String> uaList = Lists.newArrayList();
        IntStream.range(0, 10).forEach(e -> uaList.add(String.format("UA-%d", e)));
        // 洗牌
        Collections.shuffle(uaList);
        // 加载Lua脚本
        ClassPathResource resource = new ClassPathResource("/scripts/lua/L_RPOP_LPUSH.lua");
        String content = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
        String sha = COMMANDS.scriptLoad(content);
        LUA_SHA.compareAndSet(null, sha);
        // Redis队列中写入UA数据,数据量多的时候可以考虑分批写入防止长时间阻塞Redis服务
        COMMANDS.lpush(KEY, uaList.toArray(new String[0]));
    }
    @AfterClass
    public static void afterClass() throws Exception {
        COMMANDS.del(KEY);
    }
    @Test
    public void testUaPool() {
        IntStream.range(1, 21).forEach(e -> {
            String result = COMMANDS.evalsha(LUA_SHA.get(), ScriptOutputType.VALUE, KEY);
            System.out.println(String.format("第%d次获取到的UA是:%s", e, result));
        });
    }
}
复制代码


某次运行结果如下:



第1次获取到的UA是:UA-0
第2次获取到的UA是:UA-8
第3次获取到的UA是:UA-2
第4次获取到的UA是:UA-4
第5次获取到的UA是:UA-7
第6次获取到的UA是:UA-5
第7次获取到的UA是:UA-1
第8次获取到的UA是:UA-3
第9次获取到的UA是:UA-6
第10次获取到的UA是:UA-9
第11次获取到的UA是:UA-0
第12次获取到的UA是:UA-8
第13次获取到的UA是:UA-2
第14次获取到的UA是:UA-4
第15次获取到的UA是:UA-7
第16次获取到的UA是:UA-5
第17次获取到的UA是:UA-1
第18次获取到的UA是:UA-3
第19次获取到的UA是:UA-6
第20次获取到的UA是:UA-9
复制代码


可见洗牌算法的效果不差,数据相对分散。


小结



其实UA池的设计难度并不大,需要注意几个要点:


  • 一般主流的移动设备或者桌面设备的系统版本不会太多,所以来源UA数据不会太多,最简单的实现可以使用文件存放,一次读取直接写入Redis中。
  • 注意需要随机打散UA数据,避免同一个设备系统类型的UA数据过于密集,这样可以避免触发模拟某些请求时候的风控规则。
  • 需要熟悉Lua的语法,毕竟Redis的原子指令一定离不开Lua脚本。


(本文完 c-2-d e-a-20191114)

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
NoSQL Redis
Redis学习4:List数据类型、拓展操作、实现日志等
注意点:对存储空间的顺序进行分析!
Redis学习4:List数据类型、拓展操作、实现日志等
|
存储 NoSQL Redis
Redis学习3:hash类型操作、拓展操作、实现购物等
首先可以理解成一个redis里面有一个小的redis。同时要注意引入了一个field的名字。
Redis学习3:hash类型操作、拓展操作、实现购物等
|
缓存 移动开发 NoSQL
php结合redis实现高并发下的抢购、秒杀功能的实例
php结合redis实现高并发下的抢购、秒杀功能的实例
252 0
|
缓存 NoSQL 安全
2021年你还不会Shiro?----10.使用redis实现Shiro的缓存
上一篇文章已经总结了使用ehCache来实现Shiro的缓存管理,步骤也很简单,引入依赖后,直接开启Realm的缓存管理器即可。如果使用Redis来实现缓存管理其实也是一样的,我们也是需要引入redis的依赖,然后开启缓存传入自定义的redis的缓存管理器就行。区别是我们需要为自定义的redis缓存管理器提供自定义的缓存管理类。这个缓存管理类中需要使用到redisTemplate模板,这个模板我们也是需要自己定义。
264 0
2021年你还不会Shiro?----10.使用redis实现Shiro的缓存
|
NoSQL Java 关系型数据库
浅谈Redis实现分布式锁
浅谈Redis实现分布式锁
|
存储 NoSQL 关系型数据库
「Redis」事务实现机制
Redis事务实现机制
547 0
|
消息中间件 设计模式 NoSQL
异步结果通知实现——基于Redis实现,我这操作很可以
前段时间,我在内存中实现了一个简单异步通知框架。但由于没有持久化功能,应用重启就会导致数据丢失,且不支持分布式和集群。今天这篇笔记,引入了 Redis 来解决这些问题,以下是几点理由: 数据结构丰富,支持 List、Sorted Set 等 具有持久化功能,消息的可靠性能得到保证 高可用性,支持单机、主从、集群部署 项目中已使用,接入成本更低 基于 Redis 实现延时队列也有几种方法,展开详细讲讲。
|
NoSQL 前端开发 PHP
thinkphp+redis实现秒杀功能
thinkphp+redis实现秒杀功能
245 0
thinkphp+redis实现秒杀功能
|
存储 NoSQL 安全
分布式锁中-基于 Redis 的实现如何防重入
分布式锁中-基于 Redis 的实现如何防重入
247 0
分布式锁中-基于 Redis 的实现如何防重入
|
存储 消息中间件 缓存
分布式锁中-基于 Redis 的实现需避坑 - Jedis 篇
分布式锁中-基于 Redis 的实现需避坑 - Jedis 篇
337 0
分布式锁中-基于 Redis 的实现需避坑 - Jedis 篇