赞
踩
客户端经过nginx反向代理访问Tomcat服务器集群,Tomcat服务器与Redis集群和Mysql集群完成数据交互,Redis集群和Mysql集群存储数据。
Tomcat的功能组件: ①Connector:负责接收和响应请求。它是Tomcat与外界的交通枢纽,监听【Tomcat配置文件中指定的】端口(默认为8080)接收外界请求,并将请求处理后传递给容器做业务处理,最后将容器处理后的结果响应给外界。②Container:负责对内处理业务逻辑。其内部由 Engine、Host、Context和Wrapper 四个容器组成,用于管理和调用 Servlet 相关逻辑。③Service:对外提供的 Web 服务。主要包含 Connector 和 Container 两个核心组件,以及其他功能组件。Tomcat 可以管理多个 Service,且各 Service 之间相互独立。
核心:①Tomcat服务器可以访问动态资源 (静态资源和动态资源的简单区别在于:静态资源是在不修改代码的前提下任何用户任何时间访问的内容是一样的,html,css,js等,动态资源需要做数据和逻辑处理)②Nginx可以做反向代理。
实现流程:①用户在发起请求后,携带cookie,cookie中包含JSESSIONID,用于识别session,进而从Session中获取用户,判断用户是否存在判断是否登录。
②由于有一系列的Controller都需要登录验证操作,因此把该部分放到拦截器部分做。
①创建拦截器类,重写preHandle和afterCompletion函数
②在preHandle验证该用户是否存在
①验证手机号合理性②随机生成6位验证码③以String方式保存在Redis中,并设置过期时间
- @Override
- public Result sendCode(String phone, HttpSession session) {
- // 1.校验手机号(正则方式验证)
- if (RegexUtils.isPhoneInvalid(phone)) {
- // 2.如果不符合,返回错误信息
- return Result.fail("手机号格式错误!");
- }
- // 3.符合,生成验证码
- String code = RandomUtil.randomNumbers(6);
-
- // 4.保存验证码到 session
- stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
-
- // 5.发送验证码
- log.debug("发送短信验证码成功,验证码:{}", code);
- // 返回ok
- return Result.ok();
- }
- @Override
- public Result login(LoginFormDTO loginForm, HttpSession session) {
- // 1.校验手机号
- String phone = loginForm.getPhone();
- if (RegexUtils.isPhoneInvalid(phone)) {
- // 2.如果不符合,返回错误信息
- return Result.fail("手机号格式错误!");
- }
- // 3.从redis获取验证码并校验
- String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
- String code = loginForm.getCode();
- if (cacheCode == null || !cacheCode.equals(code)) {
- // 不一致,报错
- return Result.fail("验证码错误");
- }
-
- // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
- User user = query().eq("phone", phone).one();
-
- // 5.判断用户是否存在
- if (user == null) {
- // 6.不存在,创建新用户并保存
- user = createUserWithPhone(phone);
- }
-
- // 7.保存用户信息到 redis中
- // 7.1.随机生成token,作为登录令牌
- String token = UUID.randomUUID().toString(true);
- // 7.2.将User对象转为HashMap存储
- UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
- Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
- CopyOptions.create()
- .setIgnoreNullValue(true)
- .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
- // 7.3.存储
- String tokenKey = LOGIN_USER_KEY + token;
- stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
- // 7.4.设置token有效期
- stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
-
- // 8.返回token
- return Result.ok(token);
- }
核心代码分析:①通过UUID随机生成令牌。②将UserDTO转换为Map结构,属性名称作为key,具体的值作为value进行(并将他们转换位string形式)③token作为key,userMap作为Hash结构的值保存在Redis中。即通过Redis的HashMap保存UserDTO对象。④设置token有效期⑤返回token值,用于用户识别。
①创建拦截器1——检验登录并更新token有效期
- public class RefreshTokenInterceptor implements HandlerInterceptor {
-
- private StringRedisTemplate stringRedisTemplate;
-
- public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
- this.stringRedisTemplate = stringRedisTemplate;
- }
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- // 1.获取请求头中的token
- String token = request.getHeader("authorization");
- if (StrUtil.isBlank(token)) {
- return true;
- }
- // 2.基于TOKEN获取redis中的用户
- String key = LOGIN_USER_KEY + token;
- Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
- // 3.判断用户是否存在
- if (userMap.isEmpty()) {
- return true;
- }
- // 5.将查询到的hash数据转为UserDTO
- UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
- // 6.存在,保存用户信息到 ThreadLocal
- UserHolder.saveUser(userDTO);
- // 7.刷新token有效期
- stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
- // 8.放行
- return true;
- }
-
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- // 移除用户
- UserHolder.removeUser();
- }
- }
核心代码分析:①在Requset请求头中提取token②根据token,查询Redis中是否存在该用户③查询后将结果转换位UserDTO④将UsesDTO存入ThreadLocal(线程存储空间)⑤更新token有效期。
②创建拦截器2——实际检验用户是否登录,是否拦截
- public class LoginInterceptor implements HandlerInterceptor {
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- // 1.判断是否需要拦截(ThreadLocal中是否有用户)
- if (UserHolder.getUser() == null) {
- // 没有,需要拦截,设置状态码
- response.setStatus(401);
- // 拦截
- return false;
- }
- // 有用户,则放行
- return true;
- }
- }
核心代码分析:如果拦截器1一切顺利,会存入用户到ThreadLocal,如果未保存,表明用户未登录,拦截该请求。
③拦截器配置(WebMvcConfigurer)
- @Configuration
- public class MvcConfig implements WebMvcConfigurer {
-
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- // 登录拦截器
- registry.addInterceptor(new LoginInterceptor())
- .excludePathPatterns(
- "/shop/**",
- "/voucher/**",
- "/shop-type/**",
- "/upload/**",
- "/blog/hot",
- "/user/code",
- "/user/login"
- ).order(1);
- // token刷新的拦截器
- registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
- }
- }
核心代码分析:登录拦截器,配置所需拦截路径,配置拦截器执行顺序。
异常记录:创建拦截器类实现HandleInterceptor接口,重写方法时必须写@Override注解,否则拦截器不执行。
- @override
- public Result queryById (Long id) {
- string key = "cache : shop:" + id;
- //1.从redis查询商铺缓存
- string shopJson = stringRedisTemplate.opsForValue() .get(key);
- //2.判断是否存在
- if (StrUtil.isNotBlank(shopJson)) {
- //3.存在,直接返回
- Shop shop = JSONUtil.toBean( shopJson,Shop.class);
- return Result.ok(shop);
- }
- //4.不存在,根据id查询数据库
- shop shop = getById (id);
- //5.不存在,返回错误
- if (shop == null) {
- return Result.fail("店铺不存在!");
- }
- // 6.存在,写入redis
- stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
- // 7.返回
- return Result.ok(shop);
- }
核心代码分析:①以id为key,查询Redis是否有该商铺信息②有:通过JSONUtil.toBean转换为Shop对象返回直接返回 ③没有查询数据库,获得Shop对象。④以shopId作为key,Shop对象转换成Json的字符串形式作为value存入Redis。⑤返回
①内存淘汰:Redis本身的内存淘汰机制②超时剔除:ttl ③主动更新:自己手写的数据库更新策略
①读数据时,缓存未命中,查询数据库写入缓存,通过超时剔除作为缓存更新策略的兜底方案
②写数据时的问题:
- @Override
- @Transactional
- public Result update(Shop shop) {
- Long id = shop.getId();
- if (id == null) {
- return Result.fail("店铺id不能为空");
- }
- // 1.更新数据库
- updateById(shop);
- // 2.删除缓存
- stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
- return Result.ok();
- }
缓存穿透:当访问缓存和数据库中都不存在的数据时,Redis缓存未命中后会访问数据库进行查找,但当大量这样的请求同时访问时,会导致数据库崩溃,存在数据库安全的问题。(缓存空对象、布隆过滤器)
缓存雪崩:同一时间段大量的缓存key同时失效或者Redis服务宕机,导致大量的请求到达数据库,给数据库带来很大的压力。(有效期+随机值)
缓存击穿:对于热点key,被高并发访问并且重建业务叫复杂的key突然失效,无数的请求访问会在瞬间到达数据库,给数据库带来巨大的压力。(互斥锁、逻辑有效期)
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <version>30.1-jre</version>
- </dependency>
- public static void main(String[] args) {
- Config config = new Config();
- config.useSingleServer().setAddress("redis://120.48.17.2");
- config.useSingleServer().setPassword("123456");
- //构造Redisson
- RedissonClient redisson = Redisson.create(config);
-
- RBloomFilter<String> bloomFilter = redisson.getBloomFilter("随便起个名");
- //初始化布隆过滤器:预计元素为100000000L,误差率为3%
- bloomFilter.tryInit(100000000L,0.03);
- //将号码10086插入到布隆过滤器中
- bloomFilter.add("10086");
-
- //判断下面号码是否在布隆过滤器中
- //输出false
- System.out.println(bloomFilter.contains("123456"));
- //输出true
- System.out.println(bloomFilter.contains("10086"));
- }
- public <R,ID> R queryWithPassThrough(
- 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查询数据库
- R r = dbFallback.apply(id);
- // 5.不存在,返回错误
- if (r == null) {
- // 将空值写入redis
- stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
- // 返回错误信息
- return null;
- }
- // 6.存在,写入redis
- this.set(key, r, time, unit);
- 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) {
- // 将空值写入redis
- stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
- // 返回错误信息
- return null;
- }
- // 6.存在,写入redis
- this.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);
- }
核心代码分析:①Redis查询数据是否命中,命中返回。②不存在,获取锁。③.1未成功,休眠后继续尝试获取锁。③.2 成功,进行数据库和缓存重建操作(顺便缓存空值解决缓存穿透),写入Redis缓存,并返回数据。④释放锁
- private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
-
- public 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)));
- // 写入Redis
- stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
- }
- public <R, ID> R queryWithLogicalExpire(
- 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);
- // 重建缓存
- this.setWithLogicalExpire(key, newR, time, unit);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }finally {
- // 释放锁
- unlock(lockKey);
- }
- });
- }
- // 6.4.返回过期的商铺信息
- return r;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。