自媒体项目详述

总体框架

本项目主要着手于获取最新最热新闻资讯,以微服务构架为技术基础搭建校内仅供学生教师使用的校园新媒体app。以文章为主线的核心业务主要分为如下子模块。自媒体模块实现用户创建功能、文章发布功能、素材管理功能。app端用户模块实现文章搜索、文章点赞、关注、商家优惠卷秒杀等功能。业务可以帮助商家引流,增加曝光度,也可以为用户提供查看提供附近消费场所。

主要技术:SpringBoot+Springcloud+MySQL+MyBatisPlus+Redis+ElasticSearch+Kafka+MongoDB+xxl-job

主要职责:

1 采用Redis作缓存,并手动封装了一个工具类,通过互斥锁+逻辑过期的方式解决缓存击穿问题·。

在这里插入图片描述

1.1 Redis工具类

我们主要对这部分功能的java代码进行讲解:

@Slf4j    //日志
@Component//把bean给Spring维护
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//构造函数注入stringRedisTemp,也可以用resource注解public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}
//方法1  把value转为JsonStringpublic void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}
//方法2public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// 设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}
//方法3      作为工具,返回的是泛型public <R,ID> R queryWithPassThrough(//前缀,id,返回值类型,数据库查询的逻辑(有参有返回值的函数),ttl,ttl单位String keyPrefix, ID id, Class<R> 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);}//不存在// 判断命中的是否是空值if (json != null) {// 返回一个错误信息return null;}// 4.不存在,根据id查询数据库
//how从数据库查?涉及类型,sql语句等逻辑,让调用者写R r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);//方法1return r;}
//方法4public <R, ID> R queryWithLogicalExpire(//前缀,id,返回值类型,数据库查询的逻辑(有参有返回值的函数),ttl,ttl单位String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息return r;}// 5.2.已过期,需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){// 6.3.成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查询数据库R newR = dbFallback.apply(id);// 重建缓存  方法2this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {// 释放锁unlock(lockKey);}});}// 6.4.返回过期的商铺信息return r;}
//互斥锁 解决缓存击穿public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判断命中的是否是空值if (shopJson != null) {// 返回一个错误信息return null;}// 4.实现缓存重建// 4.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败,休眠并重试Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.获取锁成功,根据id查询数据库r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.释放锁unlock(lockKey);}// 8.返回return r;}
//尝试拿锁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);}
}

我们可以直接看方法4,这里通过泛型的方式对输入的参数做了以下处理:
1、从redis查询商铺缓存,判断是否存在
2、存在,需要先把json反序列化为对象
3、判断是否过期,没有过期直接返回,过期了进行缓存重建
4、过期了尝试获取锁对象,此处采用Redis中的setnx方法实现分布式锁的功能。使用店铺前缀加商品id作为key
5、没有获取到,返回过期数据。获取到了使用线程池的submit方法开启新的线程去重建缓存(也就是更新过期时间)
6、重建完成释放锁

1.2 ShopServiceImpl 中调用工具类封装的方法三解决缓存穿透

@Resource
private CacheClient cacheClient;//注入@Overridepublic Result queryById(Long id) {// 解决缓存穿透Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);// 互斥锁解决缓存击穿// Shop shop = cacheClient//         .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);// 逻辑过期解决缓存击穿// Shop shop = cacheClient//         .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);if (shop == null) {return Result.fail("店铺不存在!");}// 7.返回return Result.ok(shop);}

这里传入缓存击穿工具类的泛型参数包括:

  • CACHE_SHOP_KEY:店铺的关键字
  • id:商品的id
  • Shop.class:店铺的类型-JSON序列化时指定对象的类型
  • this::getById:方法的泛型,可以使用dbFallback.apply(传入的参数)调用,这个也可以定义为任何可执行方法
  • 20L:逻辑过期时间
  • TimeUnit.SECONDS:时间的单位

2 使用Redis作为分布式锁,解决集群下的线程并发安全问题,基于消费者组模式,完成异步下单功能。

2.1 分布式锁的概率和可能出现的问题

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路

我们可以怎么描述这个问题呢:

  • 1、解决错误删除的问题
  • 2、解决释放锁条件判断成功后出现阻塞,把别的线程释放的问题。

基本思路是使用setnx,但是为了防止死锁,我们加入过期时间。但这这样如果一个线程阻塞,会导致别的线程把自己的锁给释放,为此,我们加入了线程id的验证。但是存在一个高并发情况下有可能线程id相同的情况,为此我们加入了UUID保证每个线程的唯一。释放锁的过程必须是原子性,因为如果释放过程,条件判断是自己线程id的时候后出现阻塞的话,也会导致锁的自动释放,等别的进程来了之后刚获取的锁会被这个阻塞的释放锁过程释放,所以我们采用了lua脚本的方式保证了获取锁和释放锁的原子性。最后为了进一步提高响应速度,我们使用了异步下单的方式提高并发能力。

在这里插入图片描述

在这里插入图片描述
java代码:

利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性
private string name;//锁的名称
private StringRedisTemplate stringRedisTemplate;//构造函数 传入参数
public SimpleRedisLock( string name,StringRedisTemplate stringRedisTemplate){this.name=name;this.stringRedisTemplate = stringRedisTemplate;
}private static final String KEY_PREFIX="lock:"//key的统一前缀
@Override
public boolean tryLock(long timeoutSec) {// 获取线程标示  对应redis的value 代表 哪个线程获得锁了String threadId = Thread.currentThread().getId()// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
//封装函数 导致返回值 自动拆箱 变成 boolean 但是有null空指针风险所以用.TRUE.equals return Boolean.TRUE.equals(success);
}

2.2 Redis分布式锁误删问题

2.2.1 误删问题原因:

1、由于为了防止死锁问题,设计过期时间。当过期时间后,又有线程获取了锁对象,之前没有执行完的方法就会释放对象锁。
2、在解决1的情况下,如果释放锁的过程中出现了阻塞,那么该线程会以为自己一直是要释放自己的锁,然而却把别的刚拿到的锁给释放了。
在这里插入图片描述

2.2.2 误删解决方法

在获取锁时存入线程标示,在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致。

  1. 如果一致则释放锁
  2. 如果不一致则不释放锁
2.2.3 Lua脚本解决多条命令原子性问题

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性

lua脚本

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then-- 一致,则删除锁return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

java代码

//泛型Long是返回值类型
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {//静态代码块 初始化UNLOCK_SCRIPT = new DefaultRedisScript<>();//设置脚本 位置(ClassPathResource这个类会(默认去classpath下面找,我们的lua就放在resources下面滴 “文件名称”))UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);//返回值}public void unlock() {// 调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,//提前读取文件 等到释放锁的时候读取 会产生IO流 降低性能//.singletonList 把锁的key转化成单元素 集合形式 返回Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());//线程表示
}
经过以上代码改造后,我们就能够实现 拿锁比锁删锁的原子性动作了~

2.3分布式锁的总结

以上是我们解决分布式锁的查询的问题,该分布式锁具有以下中重点特性:
1、利用setnx方法满足互斥性
2、利用lua脚本可以实现释放锁的原子性
3、利用逻辑过期时间可以避免死锁,提高安全性
4、利用redis集群具有高可用和高并发特性

2.4 秒杀之异步下单功能

//异步处理线程池  秒杀订单 处理器
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();//在当前 Impl类初始化之后执行 ,因为当这个类初始化好了之后,随时都是有可能要执行的
@PostConstruct
private void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
// 用于线程池处理的任务
// 当初始化完毕后,就会去从队列 中去拿信息private class VoucherOrderHandler implements Runnable{@Overridepublic void run() {while (true){//不断地从队列 取try {// 1.获取队列中的订单信息take() 方法:获取 和删除 该队列的 头部,如果需要则 等待 直到 元素可用VoucherOrder voucherOrder = orderTasks.take();// 2.创建订单handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常", e);}}}private void handleVoucherOrder(VoucherOrder voucherOrder) {//1.获取用户Long userId = voucherOrder.getUserId();// 2.创建锁对象RLock redisLock = redissonClient.getLock("lock:order:" + userId);// 3.尝试获取锁boolean isLock = redisLock.lock();// 4.判断是否获得锁成功if (!isLock) {// 获取锁失败,直接返回失败或者重试log.error("不允许重复下单!");return;}try {//注意:由于是spring的事务是放在threadLocal中,此时的是多线程,事务会失效proxy.createVoucherOrder(voucherOrder);} finally {// 释放锁redisLock.unlock();}}//阻塞队列private BlockingQueue<VoucherOrder> orderTasks =new  ArrayBlockingQueue<>(1024 * 1024);@Overridepublic Result seckillVoucher(Long voucherId) {Long userId = UserHolder.getUser().getId();long orderId = redisIdWorker.nextId("order");// 1.执行lua脚本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString(), String.valueOf(orderId));int r = result.intValue();// 2.判断结果是否为0if (r != 0) {// 2.1.不为0 ,代表没有购买资格return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}//2.2 为0 有购买资格,把下单信息保存到阻塞队列VoucherOrder voucherOrder = new VoucherOrder();// 2.3.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 2.4.用户idvoucherOrder.setUserId(userId);// 2.5.代金券idvoucherOrder.setVoucherId(voucherId);// 2.6.放入阻塞队列orderTasks.add(voucherOrder);//3.子线程不能获取代理对象,主线程可以 获取了给handler创建订单proxy = (IVoucherOrderService)AopContext.currentProxy();//4.返回订单idreturn Result.ok(orderId);}@Transactional//把他的voucherOrder创建 挪到 seckillVoucherpublic  void createVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();// 5.1.查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();// 5.2.判断是否存在if (count > 0) {// 用户已经购买过了log.error("用户已经购买过了");return ;}// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣减失败log.error("库存不足");return ;}save(voucherOrder);}

代码解释:
1、主线程主要在redis中对使用lua脚本对库存、用户id判断,并将创建的订单放到队列中
2、开启线程池,对队列中的数据后台独立线程异步创建订单

2.5 异步下单总结

秒杀业务的优化思路是什么?或者可以问异步下单你做了什么工作

  • 先利用Redis完成库存余量、一人一单判断,完成抢单业务
  • 再将下单业务放入阻塞队列,利用独立线程异步下单,提高并发量

3、基于Redis中的sortedSet的排序功能,实现点赞排行榜,用时间戳作为score,筛选出最早点赞的用户

Redis的sorted_set是有序集合,在set的基础上增加score属性用来排序

点赞功能:@Overridepublic Result likeBlog(Long id) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();// 2.判断当前登录用户是否已经点赞String key = BLOG_LIKED_KEY + id;Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());if (score == null) {// 3.如果未点赞,可以点赞// 3.1.数据库点赞数 + 1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();// 3.2.保存用户到Redis的set集合  zadd key value scoreif (isSuccess) {stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());}} else {// 4.如果已点赞,取消点赞// 4.1.数据库点赞数 -1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();// 4.2.把用户从Redis的set集合移除if (isSuccess) {stringRedisTemplate.opsForZSet().remove(key, userId.toString());}}return Result.ok();}private void isBlogLiked(Blog blog) {//中间没变String key = "blog:liked:" + blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());blog.setIsLike(score != null);}
点赞列表查询
@Override
public Result queryBlogLikes(Long id) {String key = BLOG_LIKED_KEY + id;// 1.查询top5的点赞用户 zrange key 0 4Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if (top5 == null || top5.isEmpty()) {//没人点赞return Result.ok(Collections.emptyList());}// 2.解析出其中的用户idList<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());String idStr = StrUtil.join(",", ids);//3.0 未实现排序功能的点赞top5 因为传参的是5,1;
//   listByIds=in(5,1)是倒着来的, 要用 order by field/* List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());*/// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)List<UserDTO> userDTOS = userService.query()//自定义查询.in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list().stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());// 4.返回return Result.ok(userDTOS);
}

解析:
redis数据结构都是key-value结构
1、点赞时,使用博客的唯一主键作为zset的key,使用用户id作为value。这样每次点赞或者取消点赞可以把相应的用户添加或者移除。需要注意的是,我们在加入点赞信息的时候,需要加入时间戳,为后面的点赞排序做准备。
2、查看实时点赞列表时,需要查询top5的点赞用户( zrange key 0 4)

4、使用MongoDB记录用户搜索记录,使用ElasticSearch优化搜索功能,提高用户体验和减轻数据库压力。

此处是将黑马头条中的自媒体模块的搜索功能加入到了这个外卖app中。

4.1 MongoDB记录用户搜索记录

4.1.1 实体类
用户搜索记录对应的集合,对应实体类:```java
package com.heima.search.pojos;import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;import java.io.Serializable;
import java.util.Date;/*** <p>* APP用户搜索信息表* </p>* @author itheima*/
@Data
@Document("ap_user_search")
public class ApUserSearch implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/private String id;/*** 用户ID*/private Integer userId;/*** 搜索词*/private String keyword;/*** 创建时间*/private Date createdTime;}
4.1.2 搜索微服务集成mongodb

①:pom依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

②:nacos配置

spring:data:mongodb:host: 192.168.200.130port: 27017database: leadnews-history
4.1.3 创建Service中新增insert方法,并设计实现类。在实现方法中添加注解@Async来实现异步调用
@Service
@Slf4j
public class ApUserSearchServiceImpl implements ApUserSearchService {@Autowiredprivate MongoTemplate mongoTemplate;/*** 保存用户搜索历史记录* @param keyword* @param userId*/@Override@Asyncpublic void insert(String keyword, Integer userId) {//1.查询当前用户的搜索关键词Query query = Query.query(Criteria.where("userId").is(userId).and("keyword").is(keyword));ApUserSearch apUserSearch = mongoTemplate.findOne(query, ApUserSearch.class);//2.存在 更新创建时间if(apUserSearch != null){apUserSearch.setCreatedTime(new Date());mongoTemplate.save(apUserSearch);return;}//3.不存在,判断当前历史记录总数量是否超过10apUserSearch = new ApUserSearch();apUserSearch.setUserId(userId);apUserSearch.setKeyword(keyword);apUserSearch.setCreatedTime(new Date());Query query1 = Query.query(Criteria.where("userId").is(userId));query1.with(Sort.by(Sort.Direction.DESC,"createdTime"));List<ApUserSearch> apUserSearchList = mongoTemplate.find(query1, ApUserSearch.class);if(apUserSearchList == null || apUserSearchList.size() < 10){mongoTemplate.save(apUserSearch);}else {ApUserSearch lastUserSearch = apUserSearchList.get(apUserSearchList.size() - 1);mongoTemplate.findAndReplace(Query.query(Criteria.where("id").is(lastUserSearch.getId())),apUserSearch);}}
}
4.1.4 在文章搜索中加入插入搜索历史的记录
/*** es文章分页检索** @param dto* @return*/@Overridepublic ResponseResult search(UserSearchDto dto) throws IOException {//1.检查参数if(dto == null || StringUtils.isBlank(dto.getSearchWords())){return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);}ApUser user = AppThreadLocalUtil.getUser();//异步调用 保存搜索记录if(user != null && dto.getFromIndex() == 0){apUserSearchService.insert(dto.getSearchWords(), user.getId());}
4.1.5 加载搜索记录列表,在mongoDb数据中查询前十条,按照时间顺序倒叙展示

实现方法

 /*** 查询搜索历史** @return*/
@Override
public ResponseResult findUserSearch() {//获取当前用户ApUser user = AppThreadLocalUtil.getUser();if(user == null){return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);}//根据用户查询数据,按照时间倒序List<ApUserSearch> apUserSearches = mongoTemplate.find(Query.query(Criteria.where("userId").is(user.getId())).with(Sort.by(Sort.Direction.DESC, "createdTime")), ApUserSearch.class);return ResponseResult.okResult(apUserSearches);
}

4.2 使用ElasticSearch优化搜索功能

4.2.1微服务参数配置

1、在父工程pom中添加依赖

<!--elasticsearch-->
<dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.4.0</version>
</dependency>
<dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-client</artifactId><version>7.4.0</version>
</dependency>
<dependency><groupId>org.elasticsearch</groupId><artifactId>elasticsearch</artifactId><version>7.4.0</version>
</dependency>

2、nacos配置中心添加搜索微服务配置(ip和端口)

spring:autoconfigure:exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:host: 192.168.200.130port: 9200
4.2.2业务层实现类
@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {@Autowiredprivate RestHighLevelClient restHighLevelClient;/*** es文章分页检索** @param dto* @return*/@Overridepublic ResponseResult search(UserSearchDto dto) throws IOException {//1.检查参数if(dto == null || StringUtils.isBlank(dto.getSearchWords())){return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);}//2.设置查询条件SearchRequest searchRequest = new SearchRequest("app_info_article");SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();//布尔查询BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//关键字的分词之后查询QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(dto.getSearchWords()).field("title").field("content").defaultOperator(Operator.OR);boolQueryBuilder.must(queryStringQueryBuilder);//查询小于mindate的数据RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("publishTime").lt(dto.getMinBehotTime().getTime());boolQueryBuilder.filter(rangeQueryBuilder);//分页查询searchSourceBuilder.from(0);searchSourceBuilder.size(dto.getPageSize());//按照发布时间倒序查询searchSourceBuilder.sort("publishTime", SortOrder.DESC);//设置高亮  titleHighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.field("title");highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>");highlightBuilder.postTags("</font>");searchSourceBuilder.highlighter(highlightBuilder);searchSourceBuilder.query(boolQueryBuilder);searchRequest.source(searchSourceBuilder);SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);//3.结果封装返回List<Map> list = new ArrayList<>();SearchHit[] hits = searchResponse.getHits().getHits();for (SearchHit hit : hits) {String json = hit.getSourceAsString();Map map = JSON.parseObject(json, Map.class);//处理高亮if(hit.getHighlightFields() != null && hit.getHighlightFields().size() > 0){Text[] titles = hit.getHighlightFields().get("title").getFragments();String title = StringUtils.join(titles);//高亮标题map.put("h_title",title);}else {//原始标题map.put("h_title",map.get("title"));}list.add(map);}return ResponseResult.okResult(list);}
}

解析:
1、搜索的参数:关键字、时间、第几页,ES中使用must和filter连接多个搜索条件
2、所得到的hit文件中会有单独的一个hightlight属性的值(这里面设置了高亮的信息),我们将其取出后覆盖之前查询的结果,就可以实现高亮显示的效果
3、我们在覆盖是一些细节就是getFragments()来获取查询中的所有结果,结果是一个分片集合。

5 使用Kafka完成内部系统的消息通知,起到了削峰填谷及解耦的作用。

因为当时我们两个微服务是分开设计的,自媒体微服务可以先处理完,将信息发送给kafka,让app微服务异步去更新信息。
功能:
1、我们发送的是文章信息,只是topic绑定了app微服务,后期我们可以扩展更多的微服务,例如大数据之类的
2、起到削峰填谷的效果,在这里可能没有提现到。
在这里插入图片描述

5.1 导入kafka索引

1.导入kafka依赖
2、在生产者(自媒体端)的nacos配置中,加入生产者配置。在app端的nacos中配置消费者配置

生产者
spring:kafka:bootstrap-servers: 192.168.200.130:9092consumer:group-id: ${spring.application.name}key-deserializer: org.apache.kafka.common.serialization.StringDeserializervalue-deserializer: org.apache.kafka.common.serialization.StringDeserializer
消费者
spring:kafka:bootstrap-servers: 192.168.200.130:9092consumer:group-id: ${spring.application.name}key-deserializer: org.apache.kafka.common.serialization.StringDeserializervalue-deserializer: org.apache.kafka.common.serialization.StringDeserializer

5.2 监听消息,进行接下来的业务逻辑-文章上下架的功能

@Component
@Slf4j
public class ArtilceIsDownListener {@Autowiredprivate ApArticleConfigService apArticleConfigService;@KafkaListener(topics = WmNewsMessageConstants.WM_NEWS_UP_OR_DOWN_TOPIC)public void onMessage(String message){if(StringUtils.isNotBlank(message)){Map map = JSON.parseObject(message, Map.class);apArticleConfigService.updateByMap(map);log.info("article端文章配置修改,articleId={}",map.get("articleId"));}}
}

6 基于Feign熔断降级的功能,编写降级逻辑,防止服务出现雪崩问题。

  • 服务降级是服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃

  • 服务降级虽然会导致请求失败,但是不会导致阻塞。

6.1 feign远程调用是什么意思

在这里插入图片描述
feign只是定义了抽象方法,方便依赖工程可以使用相应方法,以及让别的工程实现它,做具体的事情。类似于动态代理的意思一样,把需要做的事情先抽象出方法来。
可以看到,我们在定义了feign模块,并使用了自媒体模块依赖它,在feign定义了保存接口,并由文章模块的类实现。这样的话,我们通过自动注入调用feign接口的保存方法相当于实际调用的是文章模块中的保存方法。

具体实现过程

1、feign模块定义接口

feign接口
@FeignClient(value = "leadnews-article",fallback = IArticleClientFallback.class)
public interface IArticleClient {@PostMapping("/api/v1/article/save")public ResponseResult saveArticle(@RequestBody ArticleDto dto);
}

2、在继承的模块中调用此方法

@Autowiredprivate IArticleClient articleClient;
ResponseResult responseResult = articleClient.saveArticle(dto);

3、在需要调用的模块中定一个feign接口的实现类,风格为正常controller形式

@RestController
public class ArticleClient implements IArticleClient {@Autowiredprivate ApArticleService apArticleService;@PostMapping("/api/v1/article/save")@Overridepublic ResponseResult saveArticle(@RequestBody ArticleDto dto) {return apArticleService.saveArticle(dto);}
}

4、在severce的实现类中定义具体的函数功能

@Overridepublic ResponseResult saveArticle(ArticleDto dto) {//1.检查参数if(dto == null){return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);}

6.2 feign降级逻辑的具体编写

在这里插入图片描述

实现步骤:
1、编写降级逻辑的微服务,注意这里的降级逻辑是写在feign模块中,自我理解:
因为刚才远程调用其实也是把接口实现了,我们就可以在feign模块中写另外一个实现类,在远程调用不成功的时候就可以运行另外一个实现类,当然这也是feign给我们提供的功能,具体实现是通过
2、自媒体使用feign远程调用
3、在feign远程接口(也就是你在feign中抽象出来的方法上面)中指向降级代码(接口实现类)
4、在自媒体微服务中开启服务降级功能

①:在heima-leadnews-feign-api编写降级逻辑

package com.heima.apis.article.fallback;/*** feign失败配置* @author itheima*/
@Component
public class IArticleClientFallback implements IArticleClient {@Overridepublic ResponseResult saveArticle(ArticleDto dto)  {return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"获取数据失败");}
}

在自媒体微服务中添加类,扫描降级代码类的包

package com.heima.wemedia.config;import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;@Configuration
@ComponentScan("com.heima.apis.article.fallback")
public class InitConfig {
}

②:远程接口中指向降级代码

package com.heima.apis.article;import com.heima.apis.article.fallback.IArticleClientFallback;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;@FeignClient(value = "leadnews-article",fallback = IArticleClientFallback.class)
public interface IArticleClient {@PostMapping("/api/v1/article/save")public ResponseResult saveArticle(@RequestBody ArticleDto dto);
}

③:客户端开启降级heima-leadnews-wemedia

在wemedia的nacos配置中心里添加如下内容,开启服务降级,也可以指定服务响应的超时的时间

feign:# 开启feign对hystrix熔断降级的支持hystrix:enabled: true# 修改调用超时时间client:config:default:connectTimeout: 2000readTimeout: 2000

6.3 feign远程调用的底层逻辑

底层采用是动态代理的方式实现
1.feign采用的是基于接口的注解
2.feign整合了ribbon,具有负载均衡的能力
3.整合了Hystrix,具有熔断的能力
使用: 1.添加pom依赖。
2.启动类添加@EnableFeignClients
3.定义一个接口
@FeignClient(name=“xxx”)指定调用哪个服务

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/188114.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

electron 内部api capturePage实现webview截图

官方文档 .capturePage([rect]) rect Rectangle (可选) - 要捕获的页面区域。 返回 Promise - 完成后返回一个NativeImage 在 rect内捕获页面的快照。 省略 rect 将捕获整个可见页面。 async function cap(){ let image await webviewRef.value.capturePage() console.log(im…

Android修行手册 - POI操作Excel常用样式(字体,背景,颜色,Style)

点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册点击跳转>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&…

Java学习 9.Java-数组 讲解及习题

一、数组的定义与使用 1.数组的基本概念 1.1 为什么要使用数组 数组是最简单的一种数据结构 组织一组相同类型数据的集合 数据结构本身是来描述和组织数据的 数据加结构 1.2.1 数组的创建 代码实现 new int 可省略&#xff1b; char[] chars{a,b,c};//定义一个整形类型数…

VS c++多文件编译

前言&#xff1a;记录下我这个菜鸡学习的过程&#xff0c;如有错误恳请指出&#xff0c;不胜感激&#xff01; 1.简单多文件编译调试 文件目录&#xff1a; 编译&#xff1a; -g选项是告诉编译器生成调试信息&#xff0c;这样可以在程序崩溃或出现错误时更容易地进行调试。这…

MCSM面板搭建教程和我的世界Paper服务器开服教程

雨云游戏云VPS服务器用Linux搭建MCSM面板和Minecraft Paper1.20.2服务器教程。 本教程演示安装的MC服是Paper 1.20.2版&#xff0c;其他版本也可以参考本教程&#xff0c;差别不大。 本教程使用Docker来运行mc服&#xff0c;可以方便切换不同Java版本&#xff0c;方便安装多个…

STM32——NVIC中断优先级管理分析

文章目录 前言一、中断如何响应&#xff1f;NVIC如何分配优先级&#xff1f;二、NVIC中断优先级管理详解三、问题汇总 前言 个人认为本篇文章是我作总结的最好的一篇&#xff0c;用自己的话总结出来清晰易懂&#xff0c;给小白看也能一眼明了&#xff0c;这就是写博客的意义吧…

思维模型 多看效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。越熟悉&#xff0c;越喜欢。 1 多看效应的应用 1.1 多看效应在广告和营销领域的应用 1 可口可乐之歌 可口可乐公司在 20 世纪 60 年代推出了“可口可乐之歌”广告&#xff0c;这个广告通…

PyTorch技术和深度学习——三、深度学习快速入门

文章目录 1.线性回归1&#xff09;介绍2&#xff09;加载自由泳冠军数据集3&#xff09;从0开始实现线性回归模型4&#xff09;使用自动求导训练线性回归模型5&#xff09;使用优化器训练线性回归模型 2.使用torch.nn模块构建线性回归模型1&#xff09;使用torch.nn.Linear训练…

PyCharm+Miniconda3安装配置教程

PyCharm是Python著名的Python集成开发环境&#xff08;IDE&#xff09; conda有Miniconda和Anaconda&#xff0c;前者应该是类似最小化版本&#xff0c;后者可能是功能更为强大的版本&#xff0c;我们这里安装Miniconda 按官方文档的说法conda相当于pip与virtualenv的结合&am…

R系组播调优方案

修改/etc/sysctl.conf添加如下内容&#xff1a; Vim /etc/sysctl.con net.ipv4.ip_forward1 net.ipv4.ip_nonlocal_bind1 net.ipv4.conf.all.rp_filter0 net.ipv4.conf.default.rp_filter0 net.bridge.bridge-nf-call-arptables 0 net.bridge.bridge-nf-call-ip6tables 0 …

【数据结构】树与二叉树(十):二叉树的先序遍历(非递归算法NPO)

文章目录 5.2.1 二叉树二叉树性质引理5.1&#xff1a;二叉树中层数为i的结点至多有 2 i 2^i 2i个&#xff0c;其中 i ≥ 0 i \geq 0 i≥0。引理5.2&#xff1a;高度为k的二叉树中至多有 2 k 1 − 1 2^{k1}-1 2k1−1个结点&#xff0c;其中 k ≥ 0 k \geq 0 k≥0。引理5.3&…

Python实战:绘制直方图的示例代码,数据可视化获取样本分布特征

文章目录 一、初步二、参数三、绘图类型四、多组数据直方图对比Python技术资源分享1、Python所有方向的学习路线2、学习软件3、精品书籍4、入门学习视频5、实战案例6、清华编程大佬出品《漫画看学Python》7、Python副业兼职与全职路线 一、初步 对于大量样本来说&#xff0c;如…

2023年【危险化学品经营单位主要负责人】免费试题及危险化学品经营单位主要负责人证考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年危险化学品经营单位主要负责人免费试题为正在备考危险化学品经营单位主要负责人操作证的学员准备的理论考试专题&#xff0c;每个月更新的危险化学品经营单位主要负责人证考试祝您顺利通过危险化学品经营单位主…

前端开发学习指南

前端是一个看似入门门槛不高&#xff0c;但要学好很难的领域。前端的知识体系庞杂又松散&#xff0c;技术演进快&#xff0c;如果摸不清脉络的话很容易陷入盲人摸象的困境甚至跑偏。 其实只要掌握了正确的方法&#xff0c;学习前端和学好前端就只是个时间问题&#xff0c;希望下…

OpenCV图像坐标系

绘制代码: X轴 # 选取两个点 point1 = (20, 0) point2 = (200, 0)# 在图像上绘制连接线 cv2.line(img, point1, point2, (

解决找不到x3daudio1_7.dll的方法,快速解决x3daudio1_7.dll丢失问题

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“找不到x3daudio1_7.dll”。这个问题可能是由于多种原因引起的&#xff0c;例如文件丢失、损坏或被病毒感染等。下面将详细介绍如何解决这个问题。 首先&#xff0c;我们需要了解x3daudio1_…

Kotlin HashMap entries.filter过滤forEach

Kotlin HashMap entries.filter过滤forEach fun main(args: Array<String>) {val hashMap HashMap<String, Int>()hashMap["a"] 1hashMap["b"] 2hashMap["c"] 3println(hashMap)hashMap.entries.filter {println("filter $…

2023年【北京市安全员-C3证】考试题库及北京市安全员-C3证在线考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 北京市安全员-C3证考试题库是安全生产模拟考试一点通总题库中生成的一套北京市安全员-C3证在线考试&#xff0c;安全生产模拟考试一点通上北京市安全员-C3证作业手机同步练习。2023年【北京市安全员-C3证】考试题库及…

Nignx及负载均衡动静分离

目录 一.Nginx负载均衡 1.1.下载 1.2.安装 1.3.负载均衡 二.前端部署 2.1. 准备工作 2.2.部署 好啦今天就到这里了哦&#xff01;&#xff01;&#xff01;希望能帮到你哦&#xff01;&#xff01;&#xff01; 一.Nginx负载均衡 1.1.下载 输入命令 : cd javaCloudJun/…

键盘win键无法使用,win+r不生效、win键没反应、Windows键失灵解决方案(亲测可以解决)

最近几天发现自己笔记本的win键无法使用&#xff0c;win失灵了&#xff0c;但是外接键盘后则正常:。 这个问题困扰了我一周&#xff0c;我都以为自己的枪神坏了。 寻找了几个解决方法&#xff0c;网上看了好多好多稀里糊涂的办法&#xff0c;都是不管用的&#xff0c;这里给大…