恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
黑马点评笔记
首页
资讯中心
/
黑马点评笔记
黑马点评笔记
发布时间:2026/10/1 9:58:07
记录一些敲代码过程遇到的问题和开始学的时候不熟悉的点复盘每个部分的思路前端配置nginx一开始有问题。redis要开数据库表要建好sendCode方法在userController没写userService.sendCode(),导致一直重复调用。。。低级错误不要再犯了。。短信登录1. 发送验证码用RegexUtil判断手机号码是否合法如果合法调用RandomUtil生成验证码存入session发送后返回结果。2.登录判断手机号是否有效判断验证码对不对。如果对了则在数据库中找这个用户--如果有用户则直接保存到session没有则创建后再保存。判断验证码用session内的和LoginForm比较3.登录验证拦截器验证LoginInterceptor继承HandleInterceptor实现前置拦截和渲染后拦截。·前置获取session、从session中拿出用户、比对用户判断是否存在、不存在则拦截 存在则保存到ThreadLocal。·后removeUser·配置拦截器创建mvcConfig,用拦截器注册器registry调用addInterceptorTomcat一个请求对应一个现成ThreadLocal线程本地变量可以共享数据同一个线程各个环节、不同线程直接不会造成影响。Configuration 声明一个类是配置类Spring扫描Bean方法把返回值放入Ioc容器方便自动注入。保证了Spring Bean的单例模式A调用B两个都有bean确保B只被调用一次。如果是ComponentA调用B就是普通调用会创建很多B对象Configurable是把依赖注入到非spring管理对象比较少用前端页面大小自己调了之后协议点击的地方也不在眼睛看到的按钮位置了问题1: session一开始存的实体拦截器后面调用UserHolder的UserDTO,类型不对会抛出异常-- 用BeanUtils.copyProperties转成DTO问题2:原始代码的return写了代码之后没改要return Result。注意getAttribute和setAttribute4. 用户加密由于是直接从前端拿到user可能导致信息全部被返回泄漏。将user copy到userDTO可以防止这个问题。5.改进引入redis由于用session如果一个用户登录后发多次请求可能会负载均衡到其他tomcat先前保存的信息其他tomcat看不到。登录涉及读写需要操作快一点session都存在tomcat内存可能造成空间浪费。----需要数据共享内存存储结构合理是key-value结构的。引入redis集群在tomcat之外可以存储信息。生成token从redis返回到客户端可以用string-json结构但是value还要存json格式等存储密度没hash大所以选择用hash结构存储。6. 修改· 发送验证码send Codecode存到redis· 登录时Login从redis拿到code校验如果校验成功保存用户信息、生成用户token可以用UUID转换结构为hash存入redistoken设置有效期、返回客户端·更新用户信息时间如果用户一直在活动就一直更新过期时间--拦截器判断拦截拿token比对如果用户不存在则return。因为拦截的对象是map用entries拿到所有信息注入userMap将userMap转为userDTO还是用BeanUtil,fillWithMap调用UserHolder存入ThreadLocal。最后刷新过期时间expireStringRedisTemplate的Map必须是String ,String1. 用setFieldValueEditorBeanToMap(userDTO, new HashMap(),Copyoptions.create().setIngnoreNullValue(true).setFieldValueEditor((FieldName,fieldValue)-fieldValue.toString());2. 改为userMap.put手动处理三个字段。7. 拦截器优化如果有些页面不用登录也就不用更新token时间。新加一个拦截器拦截所有--》获取token去redis比对用户信息保存到ThreadLocal刷新token原来的拦截器--》从ThreadLocal拿用户信息存在用户则拦截不存在则代表不需要拦截。拦截器逻辑别写错了refresh拦截器是如果userMap isEmpty则return true没有信息就直接放行。商户查询缓存1. 查询商户从redis查询商铺缓存-- 存在直接返回-- 不存在 查询数据库并写入redis1. redis缓存一般是json存在String结构中。java对象只存在于JVMRedis无法判别所以最后要存入Redis要转成Json形式。调用JsonUtil2.toBean(shopJSon,Shop.class) 决定了返回值类型将Json属性填进对象2. 缓存更新·更新一般有三种方法一致性低-高内存淘汰redis内存淘汰机制自动淘汰部分超时剔除设TTL、主动更新改数据库时同时更新缓存一致性就是容不容易有变化低一致性即一般不会变·主动更新选Cache Aside Pattern--选择删除缓存。更新数据库时删缓存查询再更新。更新缓存每次更新数据库都会更新无效写操作 多。--保证缓存和数据库操作同时成功或者失败单体系统缓存和数据库操作放一个事务分布式系统TCC等--线程安全先操作数据库再缓存或者 先缓存再数据库 都可能会脏读但是先数据库的概率小给shop加TTL操作先数据库再缓存: 方法写在service中用transactional3. 缓存穿透请求的数据缓存和数据库都不存在请求一直打到数据库解决缓存空对象把不存在的数据在redis写空只/布隆过滤布隆是一种算法原理是哈希函数算key比对每一位的01· 改代码的思路之前查数据库和缓存都没有直接404现在将空值写入redis同时redis命中也要判断是否为空 。isNotBlank只有在里面是字符串的时候才为true// queryById // 某个搜索信息不存在key是信息value为空 stringRedisTemplate.opsForValue().set(key,,CACHE_NULL_TTL,TimeUnit.MINUTES);如果get到null代表redis中没有这个key会访问数据库不能解决缓存穿透。“”表示有这个key但是没数据。isNotBlank判断为false于是判断shopJson null但是不是null而是“”所以直接返回错误信息。4. 缓存雪崩同一时间大量key失效或者redis宕机·解决key TTL加随机数利用redis集群提高可用性哨兵给缓存业务加降级限流策略多级缓存缓存击穿被高并发访问且缓存重建业务较复杂的key失效·解决互斥锁拿不到锁 失眠一致性逻辑过期不加ttlvalue加字段开个新线程重建缓存返回过期的数据其他的也返回过期数据可用性·代码思路缓存未命中--拿到锁--查数据库未拿到锁--等待添加锁用setnxstring用opsForValue释放锁delete//这里的Boolean是boolean的包装类直接返回可能会拆箱 // 用BoolenaUtil private boolean tryLock(String key) { Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } private void unLock(String key) { stringRedisTemplate.delete(key); }热点key加商铺缓存如果未命中说明不是热点key 返回空。再判断缓存是否过期过期了尝试获取锁获取失败返回旧数据成功开新线程创建redisdata为redis存储对象包含expireTime和Object data(不写死类型后续其他问题也可以用测试别忘了Testredis有没过期--》返回正确信息redis有过期--〉返回旧信息异步更新redis没有---》查询数据库存入redis返回数据5. 封装缓存工具两个set java对象存入key设置TTL/ 逻辑过期解决缓存击穿两个查根据key查询并反序列化为对象设置控制解决穿透 / 逻辑过期解决击穿难点主要是包装为泛型引入ID idID只是泛型。函数用dbFallback缓存失效时查询数据库public R R queryWithPassThrough( String keyPrefix, ID id, ClassR type, Function ID, R dbFallback, Long time, TimeUnit unit) { String key keyPrefix id; //1. 从redis查询缓存 String json stringRedisTemplate.opsForValue().get(key); //2. 查询缓存内有没有 if (StrUtil.isNotBlank(json)) { //3.存在直接返回 return JSONUtil.toBean(json, type); } //4. 不存在根据id查数据库 // 解决缓存穿透先判断是不是null如果不是null报错 if (json ! null) { return null; } R r dbFallback.apply(id); if (r null) { // 查到数据库还是空解决缓存穿透写空值 stringRedisTemplate.opsForValue().set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } //5. 数据写入缓存 this.set(key, r, time, unit); // 返回 return r; }Shop shop cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class.this::getById,2L,TimeUnit.MINUTES);优惠券秒杀1. 全局生成器订单主键不能自增防止规律性受单表数据量限制若多表容易出现重复全局ID生成器高可用唯一性高性能递增性。拼接信息· 实现时间戳序列号--时间戳now.toEpochSecond - 开始时间戳--序列号key自增长redis key yyyy:MM:dd分层每天创建一个key方便统计。key自增长不会涉及订单编号泄露--用左移和或运算合并两串long数字2. 秒杀下单提交优惠券id--查询信息--判读时间--开始有库存-扣库存创建订单--没有开始或者库存不足--结束3.多线程超卖解决方法就是加锁·悲观锁--上来就加锁认为安全问题一定会发生·乐观锁--安全问题不一定发生只有更新数据时去查看其他线程有没有更改没有修改则安全修改了则重试或者异常乐观锁两种方式·版本号法--更改了数据version1要修改时将前面请求的version和现version比对不一致说明被更改了·CAS法compare and set)--用数据比如库存自身对比查询库存时把正确的情况stock数量写入判断条件如果和当前数据不匹配则 不执行用CAS扣库存之前判断库存是不是和查询到的数据一样--没安全问题但失败率高并发查询时多个线程查到的数据是一样的扣库存之后数据改变导致无法正常售卖-- 数据是否相同 改成判断数据是否大于0当多个线程要对数据库id voucherId修改的时候mysql会对这一行加行锁。其他线程排队拿行锁再依次判断用jmeter测试的时候超卖。。要登录获取token之后再测试注意关注数据库等数据可能库存前面测试扣完了4. 一人一单在秒杀业务加判断如果库存充足用用户和券id查询有无订单存在订单则异常。如果并发多个线程都查询到没有订单信息就会多次下单不能实现一人一单· 解决用悲观锁查询--判断--新增不用乐观锁乐观锁是更改数据之前再查询如果对方法加synchronized可能影响性能所以可以对用户加锁·对用户加锁将一人一单--扣库存--创建订单封装为新方法createVoucherOrder如果对这个方法内部synchronized由于加了Transactional先加锁然后保存订单释放锁之后才完成整个事务。在释放锁之后可能会有其他线程拿到锁如果事务还没提交新数据就会造成重复下单。所以可以在create方法外加synchronized先获取userId由于toString底层代码是new字符串对象多次调用也会创建多个对象所以调用intern从常量池找值userId一样的字符串地址。由于return其实是this.xxxx是此对象。事务是由spring管理用的是代理对象所以获取代理对象调用create方法。可以实现事务就可以实现在synchronized内提交事务即提交之后才释放同理获取锁之后才开启事务。在启动文件加EnableAspectAutoProxy(exposeProxy true)暴露代理对象Long userId UserHolder.getUser().getId(); synchronized (userId.toString().intern()){ // 获取代理对象获取锁之后才创建事务事务提交之后才释放锁 IVoucherOrderService proxy (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); }前面只针对单机情况集群情况下可能有多个虚拟机每个jvm内有锁监视器所以可能导致无法监控所有线程多个线程同时拿到锁引发安全问题。分布式锁原理将锁监视器安在多个jvm外统一控制获取锁获取nil--获取失败获取成功--执行--释放。要实现互斥只有一个线程和非阻塞只尝试一次Override public boolean tryLock(long timeoutSec) { // 获取线程id long threadId Thread.currentThread().getId(); // redis SET lock thread1 NX EX 10,所以用ifabsent // 获取锁 Boolean success stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIXname, threadId, timeoutSec, TimeUnit.SECONDS) return Boolean.TRUE.equals(success); }函数是boolean只有tf返回是Boolean有tfnull涉及自动拆箱如果redis有异常setIfAbsent返回null可能会有安全问题--返回Boolean.TRUE.equals(success),如果success是true则返回TRUE如果是null返回false释放锁手动释放或者超时释放加个超时时间从redis删除极端情况下还是有可能发生线程安全问题所以要加判断锁的标识和当前线程是否一致不能删其他线程的锁--在获取锁时存入线程标识释放锁时检查线程标识即使有线程标识 超时释放也会导致释放其他线程的锁 用Lua脚本保证原子性if(redis.call(get, KEYS[1]) ARGV[1])then return redis.call(del, KEYS[1]) end retrun 0初始化加载脚本private static final DefaultRedisScriptLong UNLOCK_SCRIPT; static { UNLOCK_SCRIPT new DefaultRedisScript(); UNLOCK_SCRIPT.setLocation(new ClassPathResource(unlock.lua)); UNLOCK_SCRIPT.setResultType(Long.class); }分布式锁基于setnx实现还是存在问题不可重入同一个线程不那么多次获取同一个锁不可重试超时释放业务执行耗时长的话也可能引起问题主从一致性如果主没和从同步就宕机了会引发安全问题--》引入Redisson配置redisson写config将redissonClient注入bean修改业务代码用redisson获取锁tryLock可以传入参数现在要求释放不等待所以不传参waittime默认-1即不等待//SimpleRedisLock lock new SimpleRedisLock(order: userId, stringRedisTemplate); RLock lock redissonClient.getLock(lock:order: userId); // 获取锁 boolean isLock lock.tryLock(); if(!isLock){ // 获取锁失败避免一人多单 return Result.fail(不允许重复下单); } try { // 获取代理对象获取锁之后才创建事务事务提交之后才释放锁 IVoucherOrderService proxy (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } finally { lock.unlock(); }可重入锁keylockvalue用哈希结构存线程标识和重入次数获取1释放-1到方法最外面应该value为0如果为0则业务完成。之前的string类型用setnx和ex分逻辑处理锁存在--存在 判断标识 -- 设置有效期-- 锁是否是自己的value不为0重置有效期为0释放为了保证原子性还是用Lua整个流程redisson分布式锁原理可重入用了hash结构记录重入次数和线程标识可重试redisson内的pubsub机制实现了等待获取锁失败的重试机制 。释放锁会发送消息被捕捉后可重获取锁 失败--等待--重试。。超时续约获取锁成功后开启定时任务看门狗机制隔一段时间重置超时时间Redisson multiLock多个独立redis节点必须所有节点都获取重入锁才算获取锁成功秒杀优化收到请求后发给tomcat从查询到创建订单全部处理了为了提高效率可以改为多线程异步。将秒杀库存和一人一单的信息存在redis如果符合创建资格则在另一个线程创建。· 一人一单库存充足要判断用户是否下过单查找优惠券库存扣减只是扣redis数据可以改用set集合将用户if存入优惠券集合查询时直接查找用户id。lua判断库存是否充足、用户是否下过单在执行代码中引入lua脚本// 加载lua脚本 private static final DefaultRedisScriptLong SECKILL_SCRIPT; static { SECKILL_SCRIPT new DefaultRedisScript(); SECKILL_SCRIPT.setLocation(new ClassPathResource(seckill.lua)); SECKILL_SCRIPT.setResultType(Long.class); }·异步下单的思路· 用lua脚本判断用户有没有购买资格如果有就创建订单传入阻塞队列· 创建线程池线程池从阻塞队列拿信息并创建订单为什么还要创建订单lua只是减库存并把userId放到redis里面的set中没有创建数据库记录。线程池拿到信息各种id然后构建为voucherOrder对象写入数据库而且阻塞队列中的信息只是task只存在于jvm写入数据库才变成记录。lua只管和redis快速响应如果lua写数据效率会十分慢其中创建订单的流程还是创建锁判断一人一单减库存消息队列用jdk的阻塞队列可能会有内存限制或者宕机导致数据丢失所以引入消息队列redis也可以实现消息队列listpubSubstream1. listredis的list是双向链表可以用lpush rpop或者lpop rpush来模拟但是此时如果是空会返回null如果要实现阻塞效果可以用brpop和blpop优点基于redis存储不依赖jvm内存redis持久化数据安全性有保障满足消息有序性缺点pop之后没处理可能导致消息丢失只支持单消费者2. pubsub发布订阅模式可以多消费者生产者如果没人订阅会丢失消息、消息堆积有限制3. streamstream是redis的一种数据类型// 添加消息进队列-- 加入s1*表示自动生成idk1v1一组键值对 XADD s1 * k1 v1 //从stream队列s1中读取一条信息0表示从头开始$表示从最新开始 XREAD COUNT 1 STREAM s1 0 // 等待新消息等待0秒 XREAD COUNT 1 BLOCK 0 STREAM s1 0XREAD优点可回溯不会消失可以多消费者读取可以阻塞读取缺点可能漏读如果是$读取读完之后同时有很多条新消息只能读到最后的那条为了改进这些问题可以引入消费者组消费者组消息分流给不同消费者、对消息进行标识、进行消息确认消息进入pendinglist处理完执行XACK之后才会从中取出XGROUP CREATE/DESTROY... // 读消费者组g1消费者c1 读一条消息阻塞2000ms表示从下一个未消费的信息开始 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 改造代码private class VoucherOrderHandler implements Runnable{ Override public void run() { // 就是一直从消息队列取信息 while(true){ try { //获取消息队列信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS streams.orders ListMapRecordString, Object, Object list stringRedisTemplate.opsForStream().read( Consumer.from(g1, c1), StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), StreamOffset.create(queueName, ReadOffset.lastConsumed()) ); // 判断是否拿到信息 if(list null || list.isEmpty()){ // 没拿到继续循环 continue; } // 解析信息 MapRecordString, Object, Object record list.get(0); MapObject, Object values record.getValue(); VoucherOrder voucherOrder BeanUtil.fillBeanWithMap(values,new VoucherOrder(),true); // 拿到信息可以下单 handleVoucherOrder(voucherOrder); // ACK确认 stringRedisTemplate.opsForStream().acknowledge(queueName,g1,record.getId()); } catch (Exception e) { log.error(处理订单异常,e); }StreamReadOptions是Spring Data Redis 用来配置 Stream 读取行为的参数构造器设置参数时需要通过静态方法StreamReadOptions.empty()先创建一个“空的/默认的配置对象”StreamOffset.create是用来指定读取“哪一个队列”以及“从队列的什么位置开始读”的偏移量定位器。ReadOffset对应的就是0、$、MapRecord是 Spring Data Redis 专门封装 Redis Stream 消息的主体数据结构redis内信息底层由消息id和消息体键值对构成所以list.get(0)得到的是redis第一条消息getValue得到的是消息体键值对所以是Map结构。利用工具包BeanUtil将前面拿到的value拆开注入到新的voucherOrder对象自动反射注入各种id创建HandlePendingList大致和前面代码差不多如果处理订单异常则调用这个函数处理pendinglist。while ture循环如果处理完之后又有新的消息则会继续循环不用嵌套秒杀部分总结--改为异步下单引入lua脚本。将一人一单判断、扣redis库存写入lua先用lua判断如果有资格下单则将信息存入阻塞队列。开启多线程从阻塞队列中不断获取消息实现异步下单--用了阻塞队列生产者消费者通过orderTasks发送接收消息消息都存在Jvm中且是voucherorder对象重启可能会导致信息丢失--由于阻塞队列可能会有内存和数据安全问题改用消息队列用到stream结构存在redis引入消费者组增加了pendinglist处理消息只有键值对需要序列化代码思路大致如下· 类初始化之后就要执行init提交VoucherOrderHandler任务给线程池随时有人可能下单· seckillOrder引入lua判断返回判断结果· VoucherOrderHandler重写run方法任务执行流程为whiletrue一直循环从消息队列中拿信息无信息继续循环有信息反序列化调用handleVoucherOrder下单并ack确认- handleVoucherOrder从voucherOrder中获取userid获取锁反向代理使得事务生效调用createVoucherOrder执行业务事务提交之后才释放锁- createVoucherOrder真正执行业务判断一人一单、下单扣库存- 如果有异常调用handlePendingList处理达人探店查看笔记完善blogcontrollerGetMapping(/hot) public Result queryHotBlog(RequestParam(value current, defaultValue 1) Integer current) { // 根据用户查询 return blogService.queryHotBlog(current); } GetMapping(/{id}) public Result queryBlogById(PathVariable(id) Long id){ return blogService.queryBlogById(id); }·GetMapping告诉springboot只接收get方法指定URL·RequestParma告诉springboot需要且必须传入参数如果没有current参数会返回400默认1开始配合MyBatisPlus的Page对象执行分页的逻辑。Controller一般用Integer包装类而不是int防止null出现异常。·PathVarible路径变量注解将URL中的id传入方法作为参数点赞参考一人一单将blogid作为keyredis中存储给这个帖子点过赞的userId 唯一用set在Blog增加islike判断。TableField(exist false)避免MyBatisPlus的ORM映射让这些字段不强制对应表中字段登录之后的主页和点开具体帖子页面都要显示点赞的情况这两种情况都要查询代码思路获取userId-- 判断userId是否在redis key中-- 没点过赞可以点赞1且add进redis点过赞取消点赞removeLong userId UserHolder.getUser().getId();如果是游客登录没有tokengetId为null记得isMember内的id要toString,stringRedisTemplate只接受string。凡是代码里出现UserHolder.getUser()先想这个接口游客能访问吗能的话就得判空。点赞排行榜前面由于点赞唯一性用set实现排行榜要实现排序、唯一、和方便查找list结构不能实现唯一因此选用sortedset-- ZSCORE可以查询元素对应分数ZRANGE查找范围内的元素把先前关于点赞保存到redis的代码改为ZSet改完数据结构redis要清空用的是同一个key添加排行榜代码Override public Result queryBlogLikes(Long id) { String key BLOG_LIKED_KEY id; // 查询top5的点赞用户 Range 0 4 SetString top5 stringRedisTemplate.opsForZSet().range(0, 4); if(top5 null || top5.isEmpty()){ return Result.ok(Collections.emptyList()); } // 解析其中的用户id ListLong ids top5.stream().map(Long::valueOf).collect(Collectors.toList()); // 根据用户id查用户 ListUserDTO userDTOS userService.listByIds(ids) .stream() .map(user - BeanUtil.copyProperties(user,UserDTO.class)) .collect(Collectors.toList()); // 返回 return Result.ok(userDTOS);用 :: 替代 str - Long.valueOf(str)写成Long :: valueOf)。map将string转为long类型将流的数据收集到collection但是想让后点赞的人头像在前sql执行WHERE id IN(5,1)不会从5到1排序返回的是1到5sql加ORDER BY FIELD(id,5,1),代码// 根据用户id查用户ORDER BY FIELD保持ZSet里的点赞顺序listByIds会打乱顺序 // WHERE id IN(5,1) ORDER BY FIELD(id,5,1) String idStr StrUtil.join(,, ids); ListUserDTO userDTOS userService.query() .in(id, ids).last(ORDER BY FIELD(id, idStr )).list() .stream() .map(user - BeanUtil.copyProperties(user,UserDTO.class)) .collect(Collectors.toList());调用strutil的join方法将类似51拼接为字符串mybatisplus的query方法增加last表示在sql语句最后加一句关注添加两个接口关注取关判断是否关注从tb_follow中获取信息Override public Result follow(Long followUserId, Boolean isFollow) { // 现在登录用户的idfollowUserId是关注博主的id Long userId UserHolder.getUser().getId(); // 判断关注还是取关 if(isFollow){ // 没关注--加关注新增关系 Follow follow new Follow(); follow.setFollowUserId(followUserId); follow.setUserId(userId); save(follow); }else{ // 关注了--取关 // delete from tb_follow where userId ? and followUserId fuid remove(new QueryWrapperFollow() .eq(follow_user_id,followUserId).eq(user_id,userId)); } return Result.ok(); }ORM思想比如mybatisplus中数据库表一行就是一个对象Follow就是表的实体类所以要newFollowsave接收对象反射读取属性QueryWrapper负责写where子句每次查询都需要全新的条件容器所以new列名容易写错也可以用.eq(Follow::getUserId, userId)和query()不太一样// 直接通过 query() 开头链式拼接条件并直接执行 .list() 获取结果 ListBlog list blogService.query() .eq(user_id, userId) .orderByDesc(liked) .list(); // 链式结尾直接触发查询共同关注set结构求交集可以求共同关注SADD添加这个用户的关注列表SINTER s1 s2就可以查出s1s2的交集要把数据保存到redis首先改造在关注当前用户的时候不仅存储到数据库还存储到redis中关注接口add语句不要写错followUserId是关注的博主的idFeed流拉模式每个人发的信息传到发件箱需要看的时候再拉取到收件箱读比例高延时高推模式发信息直接发到粉丝收件箱写比例高适合用户量少推拉结合普通粉丝采用拉、活跃粉用推减少延迟适合用户量多修改代码思路1. 发笔记把blog存数据库之后推到粉丝收件箱2. 收件箱按时间排序用redis数据结构支持分页查询但是feed流导致数据随时发生变化尽量不用list结构不能用传统的角标分页模式sortedset支持score范围查询关注页滚动分页原理是用ZREVRANGEBYSCORE查询一共有四个参数1. max第一次查询一般是时间戳范围最大值其他是score值2. min一般写0从≦max的第一个元素开始3. offset特殊情况下如果score值相同会导致查询重复一般score有几个一样就写多少4. count查询条数时间戳是发布笔记的时间关注页要求最新发的在最上面所以从上到下时间戳由大到小。记录最小时间戳就是记录最后一条blog数据。每一次查询都是判断小于等于所以会将前面页查过的数据也一起查询因此offset就是score一样的blog条数新一轮查询时可以决定跳过多少条查询。Java 的包装类在超过-128 ~ 127时使用比较的是内存地址而不是数值本身// 解析数据blogId,offset,minTime时间戳 ListLong ids new ArrayList(typedTuples.size()); long minTime 0; int os 1; //offset for (ZSetOperations.TypedTupleString tuple: typedTuples) { // 获取id。存入zset的是string转为long存入list用add添加 ids.add(Long.valueOf(tuple.getValue())); // 获取score时间戳。将double强制转化为long类型 Long time tuple.getScore().longValue(); // Long类型的是对象指向对象的内存地址超过一定范围比较的是地址不是值long是基本数据类型 if(time minTime){ os; }else{ minTime time; os 1; } }// 根据id查blog String idStr StrUtil.join(,, ids); ListBlog blogs query().in(id, ids).last(ORDER BY FIELD(id, idStr )).list(); for (Blog blog : blogs) { // 查询笔记相关用户 queryBlogUser(blog); // 查询blog是否被点赞 isBlogLiked(blog); }将列表转为字符串相当于sql WHERE id IN (10,8,5)附近商铺基于redis 的GEO数据结构可以存储经纬度、值。GEOADD key 经 纬 valueGEODIST经纬度数据存在数据库可以存到redis中按位置查找member存shopid按照店铺类型分类写入redisTest void loadShopData(){ // 查询店铺信息 ListShop list shopService.list(); // 店铺分组按照typeid分组 MapLong, ListShop map list.stream().collect(Collectors.groupingBy(Shop::getTypeId)); // 分批写入redis for (Map.EntryLong, ListShop mapEntry : map.entrySet()) { // 获取类型id Long typeId mapEntry.getKey(); String key shop:geo: typeId; // 获取同类型店铺集合 ListShop value mapEntry.getValue(); ListRedisGeoCommands.GeoLocationString locations new ArrayList(value.size()); // GEOADD for (Shop shop : value) { locations.add(new RedisGeoCommands.GeoLocation( shop.getId().toString(), new Point(shop.getX(), shop.getY()) ) ); } stringRedisTemplate.opsForGeo().add(key,locations); } // }整体代码思路1. 判断是否需要坐标如果xy其中一个null直接按数据库查2. 分页参数from和end查redis中范围内店铺的位置信息解析信息3. skip from过滤信息实现分页将店铺id和距离存入distanceMap实现对应4. 根据shopid查询店铺但是不打乱距离排序填充每个店铺距离查询现在位置5000米圆内的店铺GeoResultsRedisGeoCommands.GeoLocationString results stringRedisTemplate.opsForGeo() // GEORADIUS key x y 5000 m WITHDIST COUNT end .radius(key, new Circle(new Point(x, y), new Distance(5000)), RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(end));ListGeoResultRedisGeoCommands.GeoLocationString list results.getContent(); if (list null || list.size() from) { // 没有下一页了返回空 return Result.ok(Collections.emptyList()); } ListLong ids new ArrayList(list.size()); MapString, Distance distanceMap new HashMap(list.size()); list.stream().skip(from).forEach(result - {因为用skip跳过实现分页查询如果跳过了所有数据list会为空最后查询出现异常因此添加判断listsize小于from不用再查询签到用数据库占用空间太大用redis bitmap一位表示一天的情况。BITMAP基于String分装到string内了