Browse Source

day10
专辑详情优化:缓存击穿分布所锁;缓存穿透:布隆过滤器

it_lv 3 weeks ago
parent
commit
53b9e66958
22 changed files with 745 additions and 13 deletions
  1. 30 4
      common/service-util/pom.xml
  2. 33 0
      common/service-util/src/main/java/com/atguigu/tingshu/common/cache/GuiGuCache.java
  3. 90 0
      common/service-util/src/main/java/com/atguigu/tingshu/common/cache/GuiGuCacheAspect.java
  4. 64 0
      common/service-util/src/main/java/com/atguigu/tingshu/common/config/redis/RedissonConfig.java
  5. 11 1
      common/service-util/src/main/java/com/atguigu/tingshu/common/config/thread/ThreadPoolConfig.java
  6. 147 0
      common/service-util/src/main/java/com/atguigu/tingshu/common/zipkin/ZipkinHelper.java
  7. 36 0
      common/service-util/src/main/java/com/atguigu/tingshu/common/zipkin/ZipkinTaskDecorator.java
  8. 27 1
      service/service-album/src/main/java/com/atguigu/tingshu/ServiceAlbumApplication.java
  9. 1 1
      service/service-album/src/main/java/com/atguigu/tingshu/album/api/AlbumInfoApiController.java
  10. 7 0
      service/service-album/src/main/java/com/atguigu/tingshu/album/api/BaseCategoryApiController.java
  11. 41 0
      service/service-album/src/main/java/com/atguigu/tingshu/album/api/TestController.java
  12. 9 1
      service/service-album/src/main/java/com/atguigu/tingshu/album/service/AlbumInfoService.java
  13. 67 1
      service/service-album/src/main/java/com/atguigu/tingshu/album/service/impl/AlbumInfoServiceImpl.java
  14. 3 0
      service/service-album/src/main/resources/application.yml
  15. 60 0
      service/service-album/src/test/java/com/atguigu/tingshu/RedissonLockTest.java
  16. 37 0
      service/service-search/src/main/java/com/atguigu/tingshu/search/receiver/SearchReceiver.java
  17. 3 0
      service/service-search/src/main/java/com/atguigu/tingshu/search/service/SearchService.java
  18. 17 2
      service/service-search/src/main/java/com/atguigu/tingshu/search/service/impl/ItemServiceImpl.java
  19. 39 0
      service/service-search/src/main/java/com/atguigu/tingshu/search/service/impl/SearchServiceImpl.java
  20. 8 1
      service/service-search/src/test/java/com/atguigu/tingshu/ServiceSearchApplicationTest.java
  21. 2 0
      service/service-user/src/main/java/com/atguigu/tingshu/user/api/UserInfoApiController.java
  22. 13 1
      service/service-user/src/test/java/com/atguigu/tingshu/ServiceUserApplicationTest.java

+ 30 - 4
common/service-util/pom.xml

@@ -52,10 +52,10 @@
         </dependency>
 
         <!-- redisson -->
-<!--        <dependency>-->
-<!--            <groupId>org.redisson</groupId>-->
-<!--            <artifactId>redisson-spring-boot-starter</artifactId>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -87,6 +87,32 @@
             <groupId>com.alibaba.cloud</groupId>
             <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-tracing-bridge-brave</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.zipkin.reporter2</groupId>
+            <artifactId>zipkin-reporter-brave</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-observation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-zipkin</artifactId>
+            <version>2.2.8.RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-micrometer</artifactId>
+            <version>12.5</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 33 - 0
common/service-util/src/main/java/com/atguigu/tingshu/common/cache/GuiGuCache.java

@@ -0,0 +1,33 @@
+package com.atguigu.tingshu.common.cache;
+
+import com.atguigu.tingshu.common.constant.RedisConstant;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+public @interface GuiGuCache {
+
+
+    /**
+     * 放入Redis缓存业务数据跟锁Key的前缀
+     * @return
+     */
+    String prefix() default "cache:";
+
+    /**
+     * 缓存过期时间
+     * @return
+     */
+    long ttl() default RedisConstant.ALBUM_TIMEOUT;
+
+    /*
+     * 时间单位
+     */
+    TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+
+}

+ 90 - 0
common/service-util/src/main/java/com/atguigu/tingshu/common/cache/GuiGuCacheAspect.java

@@ -0,0 +1,90 @@
+package com.atguigu.tingshu.common.cache;
+
+import cn.hutool.core.util.RandomUtil;
+import com.atguigu.tingshu.common.constant.RedisConstant;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+/**
+ * @author: atguigu
+ * @create: 2025-03-19 11:33
+ */
+@Slf4j
+@Component
+@Aspect
+public class GuiGuCacheAspect {
+
+    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+
+    /**
+     * 自定义缓存注解增强
+     *
+     * @param joinPoint
+     * @param guiGuCache
+     * @return
+     * @throws Throwable
+     */
+    @Around("@annotation(guiGuCache)")
+    public Object around(ProceedingJoinPoint joinPoint, GuiGuCache guiGuCache) throws Throwable {
+        try {
+            //1.优先从Redis缓存中获取业务数据,命中缓存直接返回
+            //1.1 构建业务key 形式:前缀:方法参数值(多个参数用_拼接)
+            String params = "none";
+            if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
+                params = Arrays.asList(joinPoint.getArgs())
+                        .stream()
+                        .map(Object::toString)
+                        .collect(Collectors.joining("_"));
+            }
+            String dataKey = guiGuCache.prefix() + params;
+            //1.2 从Redis中获取业务数据
+            Object result = redisTemplate.opsForValue().get(dataKey);
+            if (result != null) {
+                return result;
+            }
+            //2.获取分布式锁
+            //2.1 基于RedissonClient对象获取锁对象 入参:业务key:lock 锁粒度小一些
+            String lockKey = dataKey + RedisConstant.CACHE_LOCK_SUFFIX;
+            RLock lock = redissonClient.getLock(lockKey);
+
+            //2.2 尝试获取分布式锁
+            boolean flag = lock.tryLock();
+            if (flag) {
+                //3 获取锁成功,执行目标方法,将查库结果放入缓存,释放锁
+                try {
+                    //3.1 执行被自定义缓存注解修饰方法(查询数据方法)
+                    result = joinPoint.proceed();
+                    //3.2 将查库结果放入缓存中
+                    Long ttl = guiGuCache.ttl() + RandomUtil.randomInt(50, 1000);
+                    redisTemplate.opsForValue().set(dataKey, result, ttl, guiGuCache.timeUnit());
+                    return result;
+                } finally {
+                    //3.3 释放分布式锁
+                    lock.unlock();
+                }
+            } else {
+                //4 获取锁失败,则自旋
+                return this.around(joinPoint, guiGuCache);
+            }
+        } catch (Throwable e) {
+            //5.兜底逻辑,从数据库中查数据返回
+            log.error("自定义缓存分布式业务异常:{}", e);
+            return joinPoint.proceed();
+        }
+    }
+}

+ 64 - 0
common/service-util/src/main/java/com/atguigu/tingshu/common/config/redis/RedissonConfig.java

@@ -0,0 +1,64 @@
+package com.atguigu.tingshu.common.config.redis;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.SingleServerConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author: atguigu
+ * @create: 2025-03-19 10:15
+ */
+@Slf4j
+@Data
+@Configuration
+@ConfigurationProperties("spring.data.redis")
+public class RedissonConfig {
+
+    @Autowired
+    private RedisProperties redisProperties;
+
+    private String host;
+
+    private String password;
+
+    private String port;
+
+    private int timeout = 3000;
+    private static String ADDRESS_PREFIX = "redis://";
+
+    /**
+     * 是Redisson框架提供所有分布式服务跟分布式对象基础
+     */
+    @Bean
+    RedissonClient redissonSingle() {
+        Config config = new Config();
+
+        if (StringUtils.isBlank(host)) {
+            throw new RuntimeException("host is  empty");
+        }
+        SingleServerConfig serverConfig =
+                //Redis单节点
+                config.useSingleServer()
+                //Redis主从集群 配置所有主从节点IP端口
+                //config.useMasterSlaveServers()
+                //Redis哨兵集群: 配置所有哨兵节点IP端口
+                //config.useSentinelServers()
+                //Redis分片集群 配置所有主从节点IP跟端口
+                //config.useClusterServers()
+                .setAddress(ADDRESS_PREFIX + this.host + ":" + port)
+                .setTimeout(this.timeout);
+        if (StringUtils.isNotBlank(this.password)) {
+            serverConfig.setPassword(this.password);
+        }
+        return Redisson.create(config);
+    }
+}

+ 11 - 1
common/service-util/src/main/java/com/atguigu/tingshu/common/config/thread/ThreadPoolConfig.java

@@ -1,11 +1,15 @@
 package com.atguigu.tingshu.common.config.thread;
 
+import com.atguigu.tingshu.common.zipkin.ZipkinHelper;
+import com.atguigu.tingshu.common.zipkin.ZipkinTaskDecorator;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
-import java.util.concurrent.*;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
 
 /**
  * @author: atguigu
@@ -39,6 +43,9 @@ public class ThreadPoolConfig {
         return threadPoolExecutor;
     }*/
 
+    @Autowired(required = false)
+    private ZipkinHelper zipkinHelper;
+
 
     /**
      * 基于Spring提供线程池Class-threadPoolTaskExecutor 功能更强
@@ -65,6 +72,9 @@ public class ThreadPoolConfig {
         taskExecutor.setAwaitTerminationSeconds(300);
         // 线程不够用时由调用的线程处理该任务
         taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+
+        //设置解决zipkin链路追踪不完整装饰器对象
+        taskExecutor.setTaskDecorator(new ZipkinTaskDecorator(zipkinHelper));
         return taskExecutor;
     }
 }

+ 147 - 0
common/service-util/src/main/java/com/atguigu/tingshu/common/zipkin/ZipkinHelper.java

@@ -0,0 +1,147 @@
+package com.atguigu.tingshu.common.zipkin;
+
+import brave.Span;
+import brave.Tracer;
+import org.slf4j.MDC;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.Callable;
+
+@Component
+public class ZipkinHelper {
+    @Autowired(required = false)
+    private Tracer tracer;
+
+    public ZipkinHelper() {
+    }
+
+    public Runnable wrap(Runnable runnable) {
+        Span currentSpan = this.tracer.currentSpan();
+        return () -> {
+            Tracer.SpanInScope scope = this.tracer.withSpanInScope(currentSpan);
+            Throwable var4 = null;
+
+            try {
+                Span span = this.tracer.nextSpan();
+                MDC.put("X-B3-TraceId", span.context().traceIdString());
+                MDC.put("X-B3-SpanId", span.context().spanIdString());
+                MDC.put("X-B3-ParentSpanId", span.context().parentIdString());
+                span.name("new_thread_started").kind(Span.Kind.SERVER).tag("thread_id", Thread.currentThread().getId() + "").tag("thread_name", Thread.currentThread().getName() + "");
+                span.start();
+
+                try {
+                    Tracer.SpanInScope ws = this.tracer.withSpanInScope(span);
+                    Throwable var7 = null;
+
+                    try {
+                        runnable.run();
+                    } catch (Throwable var44) {
+                        var7 = var44;
+                        throw var44;
+                    } finally {
+                        if (ws != null) {
+                            if (var7 != null) {
+                                try {
+                                    ws.close();
+                                } catch (Throwable var43) {
+                                    var7.addSuppressed(var43);
+                                }
+                            } else {
+                                ws.close();
+                            }
+                        }
+
+                    }
+                } catch (Error | RuntimeException var46) {
+                    span.error(var46);
+                    throw var46;
+                } finally {
+                    span.finish();
+                }
+            } catch (Throwable var48) {
+                var4 = var48;
+                throw var48;
+            } finally {
+                if (scope != null) {
+                    if (var4 != null) {
+                        try {
+                            scope.close();
+                        } catch (Throwable var42) {
+                            var4.addSuppressed(var42);
+                        }
+                    } else {
+                        scope.close();
+                    }
+                }
+
+            }
+
+        };
+    }
+
+    public <T> Callable<T> wrap(Callable<T> callable) {
+        Span currentSpan = this.tracer.currentSpan();
+        return () -> {
+            Tracer.SpanInScope scope = this.tracer.withSpanInScope(currentSpan);
+            Throwable var4 = null;
+
+            T var8;
+            try {
+                Span span = this.tracer.nextSpan();
+                MDC.put("X-B3-TraceId", span.context().traceIdString());
+                MDC.put("X-B3-SpanId", span.context().spanIdString());
+                MDC.put("X-B3-ParentSpanId", span.context().parentIdString());
+                span.name("new_thread_started").kind(Span.Kind.SERVER).tag("thread_id", Thread.currentThread().getId() + "").tag("thread_name", Thread.currentThread().getName() + "");
+                span.start();
+
+                try {
+                    Tracer.SpanInScope ws = this.tracer.withSpanInScope(span);
+                    Throwable var7 = null;
+
+                    try {
+                        var8 = callable.call();
+                    } catch (Throwable var45) {
+                        var8 = (T) var45;
+                        var7 = var45;
+                        throw var45;
+                    } finally {
+                        if (ws != null) {
+                            if (var7 != null) {
+                                try {
+                                    ws.close();
+                                } catch (Throwable var44) {
+                                    var7.addSuppressed(var44);
+                                }
+                            } else {
+                                ws.close();
+                            }
+                        }
+
+                    }
+                } catch (Error | RuntimeException var47) {
+                    span.error(var47);
+                    throw var47;
+                } finally {
+                    span.finish();
+                }
+            } catch (Throwable var49) {
+                var4 = var49;
+                throw var49;
+            } finally {
+                if (scope != null) {
+                    if (var4 != null) {
+                        try {
+                            scope.close();
+                        } catch (Throwable var43) {
+                            var4.addSuppressed(var43);
+                        }
+                    } else {
+                        scope.close();
+                    }
+                }
+            }
+            return var8;
+        };
+    }
+}

+ 36 - 0
common/service-util/src/main/java/com/atguigu/tingshu/common/zipkin/ZipkinTaskDecorator.java

@@ -0,0 +1,36 @@
+package com.atguigu.tingshu.common.zipkin;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.task.TaskDecorator;
+
+/**
+
+ * @Author: chenyangu
+
+ * @Date: 2021/8/16 17:05
+
+ * @Description: zipkin装饰器
+
+ */
+
+@Slf4j
+
+public class ZipkinTaskDecorator implements TaskDecorator {
+
+    private ZipkinHelper zipkinHelper;
+
+    public ZipkinTaskDecorator(ZipkinHelper zipkinHelper) {
+
+        this.zipkinHelper = zipkinHelper;
+
+    }
+
+    @Override
+
+    public Runnable decorate(Runnable runnable) {
+
+        return zipkinHelper.wrap(runnable);
+
+    }
+
+}

+ 27 - 1
service/service-album/src/main/java/com/atguigu/tingshu/ServiceAlbumApplication.java

@@ -1,7 +1,13 @@
 package com.atguigu.tingshu;
 
+import com.atguigu.tingshu.common.constant.RedisConstant;
+import org.redisson.api.RBloomFilter;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 import org.springframework.cloud.openfeign.EnableFeignClients;
 import org.springframework.scheduling.annotation.EnableAsync;
@@ -12,10 +18,30 @@ import org.springframework.scheduling.annotation.EnableScheduling;
 @EnableFeignClients
 @EnableAsync  //开启Spring异步
 @EnableScheduling  //开启定时任务
-public class ServiceAlbumApplication {
+@EnableCaching //开启缓存
+public class ServiceAlbumApplication implements CommandLineRunner {
 
     public static void main(String[] args) {
         SpringApplication.run(ServiceAlbumApplication.class, args);
     }
 
+    @Autowired
+    private RedissonClient redissonClient;
+
+    /**
+     * 项目启动自动执行
+     * 初始化布隆过滤器:在Redis产生布隆过滤器配置
+     *
+     * @param args incoming main method arguments
+     * @throws Exception
+     */
+    @Override
+    public void run(String... args) throws Exception {
+        //1.获取布隆过滤器对象
+        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER);
+        //2.如果布隆过滤器不存在则创建,完成初始化
+        if (!bloomFilter.isExists()) {
+            bloomFilter.tryInit(100000L, 0.03);
+        }
+    }
 }

+ 1 - 1
service/service-album/src/main/java/com/atguigu/tingshu/album/api/AlbumInfoApiController.java

@@ -100,7 +100,7 @@ public class AlbumInfoApiController {
     @Operation(summary = "根据专辑ID查询专辑信息,包含标签列表")
     @GetMapping("/albumInfo/getAlbumInfo/{id}")
     public Result<AlbumInfo> getAlbumInfo(@PathVariable Long id) {
-        AlbumInfo albumInfo = albumInfoService.getAlbumInfo(id);
+        AlbumInfo albumInfo = albumInfoService.getAlbumInfoFromDB(id);
         return Result.ok(albumInfo);
     }
 

+ 7 - 0
service/service-album/src/main/java/com/atguigu/tingshu/album/api/BaseCategoryApiController.java

@@ -2,6 +2,7 @@ package com.atguigu.tingshu.album.api;
 
 import com.alibaba.fastjson.JSONObject;
 import com.atguigu.tingshu.album.service.BaseCategoryService;
+import com.atguigu.tingshu.common.cache.GuiGuCache;
 import com.atguigu.tingshu.common.result.Result;
 import com.atguigu.tingshu.model.album.BaseAttribute;
 import com.atguigu.tingshu.model.album.BaseCategory1;
@@ -36,6 +37,7 @@ public class BaseCategoryApiController {
      */
     @Operation(summary = "查询所有1,2,3级分类数据")
     @GetMapping("/category/getBaseCategoryList")
+    @GuiGuCache(prefix = "album:category:all")
     public Result<List<JSONObject>> getBaseCategoryList() {
         List<JSONObject> list = baseCategoryService.getBaseCategoryList();
         return Result.ok(list);
@@ -49,6 +51,7 @@ public class BaseCategoryApiController {
      */
     @Operation(summary = "根据一级分类Id获取分类标签以及标签值")
     @GetMapping("/category/findAttribute/{category1Id}")
+    @GuiGuCache(prefix = "album:category:attribute:")
     public Result<List<BaseAttribute>> findAttributeByCategory1Id(@PathVariable("category1Id") Long category1Id) {
         List<BaseAttribute> list = baseCategoryService.findAttributeByCategory1Id(category1Id);
         return Result.ok(list);
@@ -62,6 +65,7 @@ public class BaseCategoryApiController {
      */
     @Operation(summary = "根据三级分类ID查询分类信息")
     @GetMapping("/category/getCategoryView/{category3Id}")
+    @GuiGuCache(prefix = "album:category:category3:")
     public Result<BaseCategoryView> getCategoryView(@PathVariable("category3Id") Long category3Id) {
         BaseCategoryView baseCategoryView = baseCategoryService.getCategoryView(category3Id);
         return Result.ok(baseCategoryView);
@@ -74,6 +78,7 @@ public class BaseCategoryApiController {
      */
     @Operation(summary = "根据1级分类ID查询置顶7个三级分类列表")
     @GetMapping("/category/findTopBaseCategory3/{category1Id}")
+    @GuiGuCache(prefix = "album:category:top3:")
     public Result<List<BaseCategory3>> findTopBaseCategory3(@PathVariable("category1Id") Long category1Id) {
         List<BaseCategory3> list = baseCategoryService.findTopBaseCategory3(category1Id);
         return Result.ok(list);
@@ -87,6 +92,7 @@ public class BaseCategoryApiController {
      */
     @Operation(summary = "根据1级分类ID查询到1级分类对象(包含所属二级分类,所属三级分类列表)")
     @GetMapping("/category/getBaseCategoryList/{category1Id}")
+    @GuiGuCache(prefix = "album:category:")
     public Result<JSONObject> getBaseCategoryListByCategory1Id(@PathVariable("category1Id") Long category1Id) {
         JSONObject jsonObject = baseCategoryService.getBaseCategoryListByCategory1Id(category1Id);
         return Result.ok(jsonObject);
@@ -99,6 +105,7 @@ public class BaseCategoryApiController {
      */
     @Operation(summary = "查询所有一级分类列表")
     @GetMapping("/category/findAllCategory1")
+    @GuiGuCache(prefix = "album:category:category1")
     public Result<List<BaseCategory1>> getAllCategory1() {
         LambdaQueryWrapper<BaseCategory1> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.select(BaseCategory1::getId);

+ 41 - 0
service/service-album/src/main/java/com/atguigu/tingshu/album/api/TestController.java

@@ -0,0 +1,41 @@
+package com.atguigu.tingshu.album.api;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author: atguigu
+ * @create: 2025-03-19 14:03
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/test")
+public class TestController {
+
+    /**
+     * SpringCache 将方法上缓存注解参数作为key 方法结果作为Value
+     * @param id
+     * @return
+     */
+    @GetMapping("/{id}")
+    @Cacheable(value = "userCache", key = "#id")
+    public String queryCache(@PathVariable("id") Long id) {
+        log.info("根据ID查询数据获取业务数据");
+        return "id数据:" + id;
+    }
+
+    /**
+     * 情况缓存
+     * @param id
+     */
+    @CacheEvict(value = "userCache", key = "#id")
+    @GetMapping("/update/{id}")
+    public void updateUser(@PathVariable("id") Long id){
+        log.info("根据ID更新取业务数据");
+    }
+}

+ 9 - 1
service/service-album/src/main/java/com/atguigu/tingshu/album/service/AlbumInfoService.java

@@ -44,12 +44,18 @@ public interface AlbumInfoService extends IService<AlbumInfo> {
      */
     void removeAlbumInfo(Long id);
 
+    /**
+     * 获取专辑信息,利用缓存提高效率同时采用分布式避免缓存击穿
+     * @param id
+     * @return
+     */
+    //AlbumInfo getAlbumInfo(Long id);
     /**
      * 根据专辑ID查询专辑信息,包含标签列表
      * @param id
      * @return
      */
-    AlbumInfo getAlbumInfo(Long id);
+    AlbumInfo getAlbumInfoFromDB(Long id);
 
     /**
      * 更新专辑信息
@@ -72,4 +78,6 @@ public interface AlbumInfoService extends IService<AlbumInfo> {
      * @return
      */
     AlbumStatVo getAlbumStatVo(Long albumId);
+
+
 }

+ 67 - 1
service/service-album/src/main/java/com/atguigu/tingshu/album/service/impl/AlbumInfoServiceImpl.java

@@ -9,6 +9,8 @@ import com.atguigu.tingshu.album.mapper.TrackInfoMapper;
 import com.atguigu.tingshu.album.service.AlbumAttributeValueService;
 import com.atguigu.tingshu.album.service.AlbumInfoService;
 import com.atguigu.tingshu.album.service.VodService;
+import com.atguigu.tingshu.common.cache.GuiGuCache;
+import com.atguigu.tingshu.common.constant.RedisConstant;
 import com.atguigu.tingshu.common.constant.SystemConstant;
 import com.atguigu.tingshu.common.execption.GuiguException;
 import com.atguigu.tingshu.common.rabbit.constant.MqConst;
@@ -26,11 +28,14 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import static com.atguigu.tingshu.common.constant.SystemConstant.ALBUM_STATUS_NO_PASS;
@@ -173,6 +178,64 @@ public class AlbumInfoServiceImpl extends ServiceImpl<AlbumInfoMapper, AlbumInfo
         rabbitService.sendMessage(MqConst.EXCHANGE_ALBUM, MqConst.ROUTING_ALBUM_LOWER, id);
     }
 
+    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    /**
+     * 获取专辑信息,利用缓存提高效率同时采用分布式避免缓存击穿
+     *
+     * @param id
+     * @return
+     */
+    //@Override
+    //public AlbumInfo getAlbumInfo(Long id) {
+    //    try {
+    //        //1.优先从Redis缓存中获取业务数据,如果命中缓存直接返回
+    //        //1.1 构建专辑业务数据Key 形式:album:info:专辑ID
+    //        String dataKey = RedisConstant.ALBUM_INFO_PREFIX + id;
+    //        //1.2 获取Redis中专辑缓存数据
+    //        AlbumInfo albumInfo = (AlbumInfo) redisTemplate.opsForValue().get(dataKey);
+    //        if (albumInfo != null) {
+    //            return albumInfo;
+    //        }
+    //        //2.获取分布式锁
+    //        //2.1 创建锁对象 入参:锁名称形式:业务key:lock 锁粒度小一些
+    //        String lockKey = dataKey + RedisConstant.CACHE_LOCK_SUFFIX;
+    //        RLock lock = redissonClient.getLock(lockKey);
+    //        //2.2 尝试获取分布式锁
+    //        boolean flag = lock.tryLock();
+    //        //3.获取锁成功执行查库业务,将结果放入,业务执行完毕释放锁
+    //        if (flag) {
+    //            //3.1 从数据库中查询数据
+    //            try {
+    //                albumInfo = this.getAlbumInfoFromDB(id);
+    //                //3.2 缓存数据到Redis中 业务数据过期时间=基础时间+随机时间
+    //                Long ttl = RedisConstant.ALBUM_TIMEOUT + RandomUtil.randomInt(50, 1000);
+    //                redisTemplate.opsForValue().set(dataKey, albumInfo, ttl, TimeUnit.SECONDS);
+    //                return albumInfo;
+    //            } finally {
+    //                //3.3 在finally中释放锁
+    //                lock.unlock();
+    //            }
+    //        } else {
+    //            //4.获取锁失败,当前业务要求必须返回结果,自旋
+    //            try {
+    //                Thread.sleep(200);
+    //            } catch (InterruptedException e) {
+    //                throw new RuntimeException(e);
+    //            }
+    //            return this.getAlbumInfo(id);
+    //        }
+    //    } catch (Exception e) {
+    //        //5. 当Redis服务不可用,兜底方案:查询数据库
+    //        log.error("{},查询专辑信息异常:{}", Thread.currentThread().getName(), e);
+    //        return this.getAlbumInfoFromDB(id);
+    //    }
+    //}
+
     @Autowired
     private RabbitService rabbitService;
 
@@ -184,7 +247,8 @@ public class AlbumInfoServiceImpl extends ServiceImpl<AlbumInfoMapper, AlbumInfo
      * @return
      */
     @Override
-    public AlbumInfo getAlbumInfo(Long id) {
+    @GuiGuCache(prefix = RedisConstant.ALBUM_INFO_PREFIX, ttl = RedisConstant.ALBUM_TIMEOUT, timeUnit = TimeUnit.SECONDS)
+    public AlbumInfo getAlbumInfoFromDB(Long id) {
         //1.根据专辑ID查询专辑信息
         AlbumInfo albumInfo = baseMapper.selectById(id);
 
@@ -257,10 +321,12 @@ public class AlbumInfoServiceImpl extends ServiceImpl<AlbumInfoMapper, AlbumInfo
 
     /**
      * 根据专辑ID查询专辑统计信息
+     *
      * @param albumId
      * @return
      */
     @Override
+    @GuiGuCache(prefix = RedisConstant.ALBUM_INFO_PREFIX+"stat:")
     public AlbumStatVo getAlbumStatVo(Long albumId) {
         return baseMapper.getAlbumStatVo(albumId);
     }

+ 3 - 0
service/service-album/src/main/resources/application.yml

@@ -0,0 +1,3 @@
+spring:
+  cache:
+    type: redis # 缓存类型

+ 60 - 0
service/service-album/src/test/java/com/atguigu/tingshu/RedissonLockTest.java

@@ -0,0 +1,60 @@
+package com.atguigu.tingshu;
+
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.Test;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author: atguigu
+ * @create: 2025-03-19 10:27
+ */
+@Slf4j
+@SpringBootTest
+public class RedissonLockTest {
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+
+    @Test
+    public void testLock() {
+        RLock lock = redissonClient.getLock("lock");
+        //1.线程获取分布式锁
+        //1.1 阻塞到线程枷锁成功为止 默认过期时间:30s
+        lock.lock();
+        //1.2 阻塞到线程枷锁成功为止,释放锁时间
+        //lock.lock(5, TimeUnit.MINUTES);
+        //1.3 尝试加锁,锁空闲返回true,锁被占用返回false
+        //boolean flag = lock.tryLock();
+        //1.4 尝试加锁,设置最大等待时间,超过最大等待时间后,加锁失败,返回false
+        //boolean flag = lock.tryLock(3, TimeUnit.SECONDS);
+        //1.5 尝试加锁,设置最大等待时间,超过最大等待时间后,加锁失败,返回false.加锁成功后设置锁过期时间,到期后锁自动释放
+        //boolean flag = lock.tryLock(3, 5, TimeUnit.SECONDS);
+        //2.执行业务逻辑
+        //if (flag) {
+            try {
+                log.info(Thread.currentThread().getName()+"加锁成功,执行业务逻辑,查询数据库");
+                //TimeUnit.SECONDS.sleep(100);
+                lock.lock();
+                try {
+                    System.out.println(Thread.currentThread().getName()+"再次加锁成功,执行业务");
+                } finally {
+                    lock.unlock();
+                }
+
+
+                //tod 执行业务代码
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            } finally {
+                //3.释放锁
+                lock.unlock();
+            }
+        //} else {
+        //    log.error("加锁失败");
+        //}
+    }
+}

+ 37 - 0
service/service-search/src/main/java/com/atguigu/tingshu/search/receiver/SearchReceiver.java

@@ -1,7 +1,9 @@
 package com.atguigu.tingshu.search.receiver;
 
+import com.atguigu.tingshu.common.constant.RedisConstant;
 import com.atguigu.tingshu.common.rabbit.constant.MqConst;
 import com.atguigu.tingshu.search.service.SearchService;
+import com.atguigu.tingshu.vo.album.TrackStatMqVo;
 import com.rabbitmq.client.Channel;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -11,8 +13,11 @@ import org.springframework.amqp.rabbit.annotation.Queue;
 import org.springframework.amqp.rabbit.annotation.QueueBinding;
 import org.springframework.amqp.rabbit.annotation.RabbitListener;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Component;
 
+import java.util.concurrent.TimeUnit;
+
 /**
  * @author: atguigu
  * @create: 2025-03-12 15:50
@@ -24,6 +29,9 @@ public class SearchReceiver {
     @Autowired
     private SearchService searchService;
 
+    @Autowired
+    private RedisTemplate redisTemplate;
+
 
     /**
      * 监听专辑上架消息
@@ -65,4 +73,33 @@ public class SearchReceiver {
         }
         channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
     }
+
+
+    @SneakyThrows
+    @RabbitListener(bindings = @QueueBinding(
+            exchange = @Exchange(value = MqConst.EXCHANGE_TRACK, durable = "true"),
+            value = @Queue(value = MqConst.QUEUE_ALBUM_ES_STAT_UPDATE, durable = "true"),
+            key = MqConst.ROUTING_TRACK_STAT_UPDATE
+    ))
+    public void updateAlbumStat(TrackStatMqVo trackStatMqVo, Message message, Channel channel) {
+        if (trackStatMqVo != null) {
+            log.info("监听到更新声音统计消息,trackStatMqVo:{}", trackStatMqVo);
+            //1.消费者幂等性处理,避免同一个统计MQ消息被消费者多次消费 采用Redis set ex nx 命令实现
+            String key = RedisConstant.BUSINESS_PREFIX +"es:"+ trackStatMqVo.getBusinessNo();
+            Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.MINUTES);
+            if (flag) {
+                try {
+                    //2.调用业务逻辑层更新专辑统计信息
+                    searchService.updateAlbumStat(trackStatMqVo);
+                } catch (Exception e) {
+                    //业务处理发生异常将幂等性缓存key删除
+                    redisTemplate.delete(key);
+                    //确认消息消费失败,将消息再次入队,重新发送消息
+                    channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
+                    return;
+                }
+            }
+            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
+        }
+    }
 }

+ 3 - 0
service/service-search/src/main/java/com/atguigu/tingshu/search/service/SearchService.java

@@ -5,6 +5,7 @@ import co.elastic.clients.elasticsearch.core.SearchResponse;
 import com.atguigu.tingshu.model.search.AlbumInfoIndex;
 import com.atguigu.tingshu.model.search.SuggestIndex;
 import com.atguigu.tingshu.query.search.AlbumIndexQuery;
+import com.atguigu.tingshu.vo.album.TrackStatMqVo;
 import com.atguigu.tingshu.vo.search.AlbumInfoIndexVo;
 import com.atguigu.tingshu.vo.search.AlbumSearchResponseVo;
 
@@ -84,4 +85,6 @@ public interface SearchService {
     void updateLatelyAlbumRanking();
 
     List<AlbumInfoIndexVo> getRankingList(Long category1Id, String dimension);
+
+    void updateAlbumStat(TrackStatMqVo trackStatMqVo);
 }

+ 17 - 2
service/service-search/src/main/java/com/atguigu/tingshu/search/service/impl/ItemServiceImpl.java

@@ -2,6 +2,8 @@ package com.atguigu.tingshu.search.service.impl;
 
 import cn.hutool.core.lang.Assert;
 import com.atguigu.tingshu.album.AlbumFeignClient;
+import com.atguigu.tingshu.common.constant.RedisConstant;
+import com.atguigu.tingshu.common.execption.GuiguException;
 import com.atguigu.tingshu.model.album.AlbumInfo;
 import com.atguigu.tingshu.model.album.BaseCategoryView;
 import com.atguigu.tingshu.search.service.ItemService;
@@ -9,6 +11,8 @@ import com.atguigu.tingshu.user.client.UserFeignClient;
 import com.atguigu.tingshu.vo.album.AlbumStatVo;
 import com.atguigu.tingshu.vo.user.UserInfoVo;
 import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RBloomFilter;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -33,13 +37,24 @@ public class ItemServiceImpl implements ItemService {
     @Autowired
     private Executor threadPoolTaskExecutor;
 
+    @Autowired
+    private RedissonClient redissonClient;
+
     /**
      * 根据专辑ID查询专辑详情
+     *
      * @param albumId
      * @return
      */
     @Override
     public Map<String, Object> getItemData(Long albumId) {
+        //0.判断访问专辑在布隆器是否存在
+        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER);
+        boolean contains = bloomFilter.contains(albumId);
+        //true:说明可能存在
+        if (!contains) {
+            throw new GuiguException(404, "专辑不存在");
+        }
         //1.创建封装专辑详情相关数据Map 存在多线程并发写,选择线程安全集合类HashTable,ConcurrentHashMap
         Map<String, Object> map = new ConcurrentHashMap<>();
         //2.远程调用专辑服务获取专辑信息-封装专辑信息
@@ -48,7 +63,7 @@ public class ItemServiceImpl implements ItemService {
             Assert.notNull(albumInfo, "专辑:{}不存在", albumId);
             map.put("albumInfo", albumInfo);
             return albumInfo;
-        }, threadPoolTaskExecutor).exceptionally(e->{
+        }, threadPoolTaskExecutor).exceptionally(e -> {
             log.error("查询专辑详情失败:{}", e.getMessage());
             return null;
         }).orTimeout(1, TimeUnit.SECONDS);
@@ -68,7 +83,7 @@ public class ItemServiceImpl implements ItemService {
         }, threadPoolTaskExecutor);
 
         //5.远程调用用户服务获取主播信息-封主播信息
-        CompletableFuture<Void> announcerCompletableFuture  = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> {
+        CompletableFuture<Void> announcerCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> {
             UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(albumInfo.getUserId()).getData();
             Assert.notNull(userInfoVo, "主播:{}不存在", albumInfo.getUserId());
             map.put("announcer", userInfoVo);

+ 39 - 0
service/service-search/src/main/java/com/atguigu/tingshu/search/service/impl/SearchServiceImpl.java

@@ -17,9 +17,11 @@ import co.elastic.clients.elasticsearch.core.search.CompletionSuggestOption;
 import co.elastic.clients.elasticsearch.core.search.Hit;
 import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
 import co.elastic.clients.elasticsearch.core.search.Suggestion;
+import co.elastic.clients.json.JsonData;
 import com.alibaba.fastjson.JSON;
 import com.atguigu.tingshu.album.AlbumFeignClient;
 import com.atguigu.tingshu.common.constant.RedisConstant;
+import com.atguigu.tingshu.common.constant.SystemConstant;
 import com.atguigu.tingshu.model.album.*;
 import com.atguigu.tingshu.model.search.AlbumInfoIndex;
 import com.atguigu.tingshu.model.search.AttributeValueIndex;
@@ -29,11 +31,14 @@ import com.atguigu.tingshu.search.repository.AlbumInfoIndexRepository;
 import com.atguigu.tingshu.search.repository.SuggestIndexRepository;
 import com.atguigu.tingshu.search.service.SearchService;
 import com.atguigu.tingshu.user.client.UserFeignClient;
+import com.atguigu.tingshu.vo.album.TrackStatMqVo;
 import com.atguigu.tingshu.vo.search.AlbumInfoIndexVo;
 import com.atguigu.tingshu.vo.search.AlbumSearchResponseVo;
 import com.atguigu.tingshu.vo.user.UserInfoVo;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.redisson.api.RBloomFilter;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.elasticsearch.core.suggest.Completion;
 import org.springframework.data.redis.core.BoundHashOperations;
@@ -72,6 +77,9 @@ public class SearchServiceImpl implements SearchService {
     @Autowired
     private SuggestIndexRepository suggestIndexRepository;
 
+    @Autowired
+    private RedissonClient redissonClient;
+
     //@Autowired
     //@Qualifier("threadPoolTaskExecutor")
     //private Executor executor;
@@ -161,6 +169,10 @@ public class SearchServiceImpl implements SearchService {
 
         //8.保存专辑名称到提词索引库
         this.saveSuggest(albumInfoIndex);
+
+        //9.将上架专辑ID存入布隆过滤器
+        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER);
+        bloomFilter.add(albumId);
     }
 
     /**
@@ -567,4 +579,31 @@ public class SearchServiceImpl implements SearchService {
         }
         return null;
     }
+
+    /**
+     * 更新专辑统计数据
+     *
+     * @param trackStatMqVo
+     */
+    @Override
+    public void updateAlbumStat(TrackStatMqVo trackStatMqVo) {
+        try {
+            Long albumId = trackStatMqVo.getAlbumId();
+            String incrementField = "";
+            if(SystemConstant.TRACK_STAT_PLAY.equals(trackStatMqVo.getStatType())){
+                incrementField = "playStatNum";
+
+            }else if(SystemConstant.TRACK_STAT_COMMENT.equals(trackStatMqVo.getStatType())){
+                incrementField = "commentStatNum";
+            }
+            String finalIncrementField = incrementField;
+            elasticsearchClient.update(
+                    u -> u.index(INDEX_NAME).id(albumId.toString())
+                            .script(s -> s.inline(i -> i.lang("painless").source("ctx._source."+ finalIncrementField +" += params.increment").params(Map.of("increment", JsonData.of(trackStatMqVo.getCount())))))
+                    , AlbumInfoIndex.class
+            );
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }

+ 8 - 1
service/service-search/src/test/java/com/atguigu/tingshu/ServiceSearchApplicationTest.java

@@ -1,11 +1,14 @@
 package com.atguigu.tingshu;
 
 import com.atguigu.tingshu.album.AlbumFeignClient;
+import com.atguigu.tingshu.common.constant.RedisConstant;
 import com.atguigu.tingshu.common.result.Result;
 import com.atguigu.tingshu.model.album.AlbumInfo;
 import com.atguigu.tingshu.search.service.SearchService;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.jupiter.api.Test;
+import org.redisson.api.RBloomFilter;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
@@ -27,14 +30,18 @@ class ServiceSearchApplicationTest {
     private SearchService searchService;
 
 
+    @Autowired
+    private RedissonClient redissonClient;
     /**
      * 不严谨:循环从11700批量导入索引库
      */
     @Test
     public void testBatchImport(){
+        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER);
         for (long i = 1; i <=1617 ; i++) {
             try {
-                searchService.upperAlbum(i);
+                //searchService.upperAlbum(i);
+                bloomFilter.add(i);
             } catch (Exception e) {
                 e.printStackTrace();
                 continue;

+ 2 - 0
service/service-user/src/main/java/com/atguigu/tingshu/user/api/UserInfoApiController.java

@@ -1,5 +1,6 @@
 package com.atguigu.tingshu.user.api;
 
+import com.atguigu.tingshu.common.cache.GuiGuCache;
 import com.atguigu.tingshu.common.result.Result;
 import com.atguigu.tingshu.user.service.UserInfoService;
 import com.atguigu.tingshu.vo.user.UserInfoVo;
@@ -22,6 +23,7 @@ public class UserInfoApiController {
 
     @Operation(summary = "查询用户基本信息")
     @GetMapping("/userInfo/getUserInfoVo/{userId}")
+    @GuiGuCache(prefix = "user:userinfo:")
     public Result<UserInfoVo> getUserInfoVo(@PathVariable("userId") Long userId) {
         UserInfoVo userInfoVo = userInfoService.getUserInfo(userId);
         return Result.ok(userInfoVo);

+ 13 - 1
service/service-user/src/test/java/com/atguigu/tingshu/ServiceUserApplicationTest.java

@@ -4,8 +4,9 @@ import com.atguigu.tingshu.common.rabbit.service.RabbitService;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.redis.core.RedisTemplate;
 
-import static org.junit.jupiter.api.Assertions.*;
+import java.util.Set;
 
 @SpringBootTest
 class ServiceUserApplicationTest {
@@ -19,4 +20,15 @@ class ServiceUserApplicationTest {
         rabbitService.sendMessage("exchange.order", "order.create", "hello");
     }
 
+    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @Test
+    public void testZset(){
+        //对zset中mysql元素分增量累加
+        //redisTemplate.opsForZSet().incrementScore("runoobkey", "mysql", 10);
+
+        Set runoobkey = redisTemplate.opsForZSet().reverseRange("runoobkey", 0, 2);
+        System.out.println(runoobkey);
+    }
 }