黑马点评项目的学习日志 项目需要实现的功能介绍
项目架构
前端登录 ,使用nginx启动前端项目 ,然后访问8080端口,必须是在后端项目启动的情况下
数据转换 Bean --- > String :
Bean.toString()
String ----> Bean :
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
Bean ---->hashMap :
Map<String, Object> map1 = BeanUtil.beanToMap(userDTO);
hash ---->Bean :
UserDTO userDTO1 = BeanUtil.mapToBean(map, UserDTO.class, true);
2023.2.18—–短信登录
基于session实现发送验证码登录
流程图的分析 发送短信验证码
接口:@PostMapping("/user/code")
1 2 3 4 public Result sendCode (@RequestParam("phone") String phone, HttpSession session) { return userService.sendCode(phone,session); }
service层实现流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Override public Result sendCode (String phone, HttpSession session) { boolean isNotNumber = RegexUtils.isPhoneInvalid(phone); if (isNotNumber){ return Result.fail("手机号格式错误 !!!" ); } String code = RandomUtil.randomNumbers(6 ); session.setAttribute("code" ,code); log.debug("发送短信验证码成功 !" ); return Result.ok(); }
短信验证码登录
提交验证码和手机号,并且进行判断是否正确
正确 : 就继续 , 错误 : 返回验证码错误
调用数据库查询用户是否存在
存在的话 : 保存用户信息到session,不存在 : 就跳转到注册页面 ,注册并保存到数据库
接口 : @PostMapping("/user/login")
1 2 3 4 5 @PostMapping("/login") public Result login (@RequestBody LoginFormDTO loginForm, HttpSession session) { return userService.login(loginForm,session); }
service层实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override public Result login (LoginFormDTO loginForm, HttpSession session) { String phone = (String) session.getAttribute("phone" ); String code = (String) session.getAttribute("code" ); if (!loginForm.getPhone().equals(phone) || !loginForm.getCode().equals(code)){ return Result.fail("手机号/验证码错误!" ); } User user = query().eq("phone" , phone).one(); if (user == null ){ user = createUserWithPhone(phone); } session.setAttribute("user" ,user); return Result.ok(); } private User createUserWithPhone (String phone) { User user = new User (); user.setPhone(phone); user.setNickName("user_" + RandomUtil.randomString(10 )); return user; }
校验登录状态
在拦截器中是实现校验功能
流程
获取请求携带的cookie
获取用户
判断用户是否存在,存在 : 保存该线程 ,**不存在 :**拦截
首先实现拦截器 在拦截器中我们就可以实现我们需要的登录流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.hmdp.config.Handler;import com.hmdp.dto.UserDTO;import com.hmdp.entity.User;import com.hmdp.utils.UserHolder;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object user = session.getAttribute("user" ); if (user == null ){ response.setStatus(401 ); return false ; } UserHolder.saveUser((UserDTO) user); return true ; } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
在mvc配置类中添加拦截器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.hmdp.config;import com.hmdp.config.Handler.LoginInterceptor;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor ()) .excludePathPatterns( "/user/code" , "/user/login" , "/blog/hot" , "/shop/**" , "/shop-type/**" , "/upload/**" , "/voucher/**" ); } }
登录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Override public Result login (LoginFormDTO loginForm, HttpSession session) { String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)){ return Result.fail("手机号格式错误!" ); } String code = (String) session.getAttribute("code" ); if (phone == null || !loginForm.getCode().equals(code)){ return Result.fail("手机号/验证码错误!" ); } User user = query().eq("phone" , phone).one(); if (user == null ){ user = createUserWithPhone( phone); } UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); session.setAttribute("user" ,userDTO); return Result.ok(); }
对于返回的信息,因为我们登录时会设置密码等 ,一系列隐私属性,如果我们返回前端这些属性的话,那么势必会造成信息泄露。所以为例用户安全着想 我们映射到前端的时候不能将全部的信息都返回 。 仅仅返回一些简单的信息即可,所以我们就用到了UserDTO
。 使用Bean.copyProperties
就可以将user中的属性的值拷贝一份给userDto
。 然后存储到session的就是我们的UserDTo
对象, 这样就避免用户信息在传入前端时出现信息泄露的风险。
获取当前用户并返回 1 2 3 4 5 6 @GetMapping("/me") public Result me () { UserDTO user = UserHolder.getUser(); return Result.ok(user); }
集群的Session共享问题 存储的信息是在单线程中的,所以多台Tomcat并不能共享session的存储空间 ,当请求切换到不同的tomcat服务器导致数据丢失的问题 ——session共享问题
当用户第一次进入系统时,tomcat服务器① 接收到请求,然后进行处理用户的请求(登录注册等)。
当用户第二次进入系统时 ,被负载均衡到了tomca服务器② 。用户的信息其实是已经注册了的,但是这里却无法获取。导致用户还得注册…这会造成用户体验感很差,所以我们需要继续处理。
这就是Session共享的问题
解决办法:
实现session共享。
Redis实现解决Session共享问题
使用redis代替session解决。
基于Redis实现共享session登录
当我们实现生成验证码时 ,就可以直接将以 【手机号为 key : 验证码为value】
保存到reids中
输入完验证码,点击登录的时候。进行校验,我们就可以在redis中查询当前手机号保存的value与输入的进行比较。如果比较正确就下一步
根据手机号查询是否有这个用户。如果有就保存这个用户的信息到redis ,【以随机的token为key : 用户信息为value】
。
如果没有查到,那么就注册新的用户。然后保存到数据库(mysql),然后再回到步骤3 进行
校验登录状态时, 请求就会携带着token。
然后我们就可以通过携带得token得value属性值判断用户是否存在。如果存在,那么就保存用户到ThreadLocal
中,然后放行该请求。
如果不存在,那么拦截器就会拦截请求
实现
发送短信验证码,然后将验证码保存到redis中
1 2 3 4 5 6 7 8 String code = RandomUtil.randomNumbers(6 );stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Resource private StringRedisTemplate stringRedisTemplate;@Autowired private UserMapper userMapper;@Override public Result sendCode (String phone, HttpSession session) { boolean isNotNumber = RegexUtils.isPhoneInvalid(phone); if (isNotNumber){ return Result.fail("手机号格式错误 !!!" ); } String code = RandomUtil.randomNumbers(6 ); stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES); log.debug("发送短信验证码成功 ! 验证码为 : [" + code + "]" ); return Result.ok(); }
点击登录的时候。进行校验,我们就可以在redis中查询当前手机号保存的value与输入的进行比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Override public Result login (LoginFormDTO loginForm, HttpSession session) { String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)){ return Result.fail("手机号格式错误!" ); } String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); if (phone == null || !loginForm.getCode().equals(code)){ return Result.fail("验证码错误!" ); } User user = query().eq("phone" , phone).one(); if (user == null ){ user = createUserWithPhone( phone); } String token = UUID.randomUUID().toString(true ); UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap <>(), CopyOptions.create().setIgnoreNullValue(true ). setFieldValueEditor((filedName , fieldValue) -> fieldValue.toString())); stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map); stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL ,TimeUnit.MINUTES); return Result.ok(token); }
重点1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 String token = UUID.randomUUID().toString(true );UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap <>(), CopyOptions.create().setIgnoreNullValue(true ) .setFieldValueEditor((filedName , fieldValue) -> fieldValue.toString())); stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map); stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL ,TimeUnit.MINUTES);
重点2 重置token的时常,实现30分钟为点击删除token 以及**一旦点击某个请求就重置token的时间
**
方法: 新增一个拦截器,只处理点击请求就重置token时间的问题
两个不同作用的拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.hmdp.config.Handler;import cn.hutool.core.bean.BeanUtil;import cn.hutool.core.util.StrUtil;import com.hmdp.dto.UserDTO;import com.hmdp.utils.UserHolder;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.Map;import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;public class preHandler implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public preHandler (StringRedisTemplate stringRedisTemplate) { this .stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("authorization" ); String key = LOGIN_USER_KEY+ token; if (StrUtil.isBlank(token)){ return true ; } Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(key); UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO (), false ); UserHolder.saveUser(userDTO); stringRedisTemplate.expire(key,LOGIN_USER_TTL , TimeUnit.MINUTES); return true ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.hmdp.config.Handler;import com.hmdp.utils.UserHolder;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (UserHolder.getUser() == null ){ response.setStatus(401 ); return false ; } return true ; } }
MVC配置文件中设置拦截器的执行顺序
通过后面的order(x); //x数字越小优先级越高,越先执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor ()) .excludePathPatterns( "/user/code" , "/user/login" , "/blog/hot" , "/shop/**" , "/shop-type/**" , "/upload/**" , "/voucher/**" ).order(1 ); registry.addInterceptor(new preHandler (stringRedisTemplate)).addPathPatterns("/**" ).order(0 ); } }
2023.2.20—–商户查询缓存 什么是缓存 数据交换的缓存区,是存储数据的临时地方,一般读写效率高
web应用中:
缓存在web应用中,缓存可以降低后端的负载、提高读写效率、降低响应时间
成本 : 数据的一致性成本、代码维护成本、运维成本…..
如何添加缓存 业务流程分析 与 模型
从redis中查询商铺的缓存
判断redis中是否存在该id的商户
如果存在 : 返回商户的信息
如果不存在: 根据传入的id查询数据库 ,判断数据库中是否存在商户,如果不存在就返回401
数据库中 商户如果存在就将商户信息写入redis
返回商户信息
接口:
1 2 3 4 5 6 7 8 9 @GetMapping("/{id}") public Result queryShopById (@PathVariable("id") Long id) { return shopService.queryById(id); }
Service层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Service public class ShopServiceImpl extends ServiceImpl <ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryById (Long id) { String Iid = String.valueOf(id); String shopJson = stringRedisTemplate.opsForValue().get(Iid); if (StrUtil.isNotBlank(shopJson)){ Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } Shop shopN = getById(id); String key = CACHE_SHOP_KEY + id; if (shopN == null ){ return Result.fail("店铺不存在!" ); } stringRedisTemplate.opsForValue().set(key,shopN.toString()); return Result.ok(shopN); } }
给商品类型添加redis缓存
service层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Service public class ShopTypeServiceImpl extends ServiceImpl <ShopTypeMapper, ShopType> implements IShopTypeService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryTypeList () { ShopType shopType = new ShopType (); List<String> range = stringRedisTemplate.opsForList().range("shopTypeList" , 0 , -1 ); if (!range.isEmpty()){ return Result.ok(range); } List<ShopType> sort = query().orderByAsc("sort" ).list(); for (ShopType item : sort){ stringRedisTemplate.opsForList().rightPush("shopTypeList" , JSONUtil.toJsonStr(item)); } List<String> typeList = stringRedisTemplate.opsForList().range("shopTypeList" , 0 , -1 ); return Result.ok(typeList); } }
缓存更新策略- —双写一致性问题
解决数据同步的问题
解决策略
场景:
低一致性需求: 使用内存淘汰机制,例如店铺类型的查询缓存
高一致性需求: 主动更新,并且使用超时剔除作为兜底方案。例如店铺的详情查询
主动更新的策略
由缓存的调用者,在更新数据库的同时更新缓存
(常用!)
缓存与数据库整合为一个服务,由服务来维护一致性。,调用者无需关系缓存一致性问题
调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终的一致
主动更新策略的考虑问题 删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新缓存
如何保证缓存与数据库的操作的同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务
分布式系统,利用TCC等
分布式事务方案先操作缓存还是先操作数据库?
缓存更新策略的最佳实践方案: 低一致性需求:
高一致性需求:
主动更新,并以超时剔除作为兜底方案
读操作:缓存命中则直接返回缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:先写数据库,然后再删除缓存要确保数据库与缓存操作的原子性
操作: 查询商户时设置超时删除策略
1 2 stringRedisTemplate.opsForValue().set(key,shopN.toString(),30 , TimeUnit.MINUTES);
每次更新数据时,就会先删除缓存,然后再从次查询时会先从数据库中查出更新过的数据保存到缓存中去,然后再回显
1 2 3 4 5 6 7 8 9 10 @PutMapping public Result updateShop (@RequestBody Shop shop) { return shopService.update(shop); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override @Transactional public Result update (Shop shop) { updateById(shop); if (shop.getId() == null ){ return Result.fail("店铺id不能为空!!!" ); } Long id = shop.getId(); String key = CACHE_SHOP_KEY + id; stringRedisTemplate.delete(key); return Result.ok(); } }
缓存穿透
指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
解决办法
缓存空对象 : 如果用户恶意多次查找数据库和缓存中都不存在的对象,我们可以给这种对象赋一个空的对象到redis,这样无论多少次恶意请求,他都不会多次访问数据库,只要一次访问不到,那么就只能到缓存中拿空对象了
优点:实现简单,维护方便
缺点:额外的内存消耗可能造成短期的不一致
控制ttl时间,可以实现短期不一致的降低
布隆过滤
优点 :内存占用非常小
缺点: 实现复杂、存在误判的可能
实现
如果用户第一次查询,没有从缓存和数据库中查出数据,那么就创建一个空值(key1, “” ),存入redis
后面如果再次查询不存在的用户key1,那么就可以从缓存中查询将空值拿出来,然后直接返回,这样就可以不用操作数据库
如果2中拿出来的值为null,那么就说明店铺
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 String key = CACHE_SHOP_KEY + id;if (shopJson != null ){ return Result.fail("店铺不存在!" ); } Shop shopN = getById(id);if (shopN == null ){ stringRedisTemplate.opsForValue().set(key,"" ,CACHE_NULL_TTL,TimeUnit.MINUTES); return Result.fail("店铺不存在!" ); }
整个逻辑的代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @Override public Result queryById (Long id) { String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)){ Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } if (shopJson != null ){ return Result.fail("店铺不存在!" ); } Shop shopN = getById(id); String key = CACHE_SHOP_KEY + id; if (shopN == null ){ stringRedisTemplate.opsForValue().set(key,"" ,CACHE_NULL_TTL,TimeUnit.MINUTES); return Result.fail("店铺不存在!" ); } stringRedisTemplate.opsForValue().set(key,shopN.toString(),30 , TimeUnit.MINUTES); return Result.ok(shopN); }
缓存穿透产生的原因是什么? 用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些? 缓存null值、布隆过滤、增强id的复杂度,避免被猜测id规律、做好数据的基础格式校验、加强用户权限校验、做好热点参数的限流
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机 ,导致大量请求到达数据库 ,带来巨大压力。
服务延机最为可怕。
解决方案:
给不同的Key的TTL添加随机值 、利用Redis集群提高服务的可用性、给缓存业务添加降级限流策略、给业务添加多级缓存
(后面的三个暂未实现)
1 String key = CACHE_SHOP_KEY + id + RandomUtil.randomInt(4 );
缓存击穿
缓存击穿问题也叫热点Key问题 ,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方法(需要在一致性和可用性上做出选择) 互斥锁 优点 :
缺点:
从redis中查询数据,如果没查到,那么就返回null,到数据库中找,然后返回
如果找到了
判断缓存是否过期,未过期那就返回,并说明找到了
如果缓存过期了
尝试获取互斥锁,并判断是否获取到了互斥锁
如果获取到了,那么就开启独立线程,然后再从数据库中找打,然后写入redis并设置逻辑过期时间
释放互斥锁
逻辑过期 优点:
缺点:
实现 互斥锁的方式:
1 2 3 4 5 6 7 8 9 10 11 12 @Override public Result queryById (Long id) { Shop shop = queryWithMutex(id); if (shop == null ){ return Result.fail("店铺不存在!!!" ); } return Result.ok(shop); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 public Shop queryWithMutex (Long id) { String key = CACHE_SHOP_KEY + id; System.out.println(key); String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)){ Shop shop = JSONUtil.toBean(shopJson, Shop.class); return shop; } if (shopJson != null ){ return null ; } Shop shopN = null ; String lockKey = LOCK_SHOP_KEY + id; try { boolean isLock = tryLock(lockKey); if (!isLock){ Thread.sleep(50 ); queryWithMutex(id); } shopN = getById(id); if (shopN == null ){ stringRedisTemplate.opsForValue().set(key,"" ,CACHE_NULL_TTL,TimeUnit.MINUTES); return null ; } stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopN),30 , TimeUnit.MINUTES); }catch (InterruptedException e){ throw new RuntimeException (e); } finally { unLock(lockKey); } return shopN; } boolean tryLock (String key) { Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1" , 10 , TimeUnit.MINUTES); return BooleanUtil.isTrue(aBoolean); } void unLock (String key) { stringRedisTemplate.delete(key); } public Shop queryWithPassThrough (Long id) { String key = CACHE_SHOP_KEY + id; System.out.println(key); String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)){ Shop shop = JSONUtil.toBean(shopJson, Shop.class); return shop; } if (shopJson != null ){ return null ; } Shop shopN = getById(id); if (shopN == null ){ stringRedisTemplate.opsForValue().set(key,"" ,CACHE_NULL_TTL,TimeUnit.MINUTES); return null ; } stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopN),30 , TimeUnit.MINUTES); return shopN; }
方法二: 逻辑过期解决缓存击穿
暂未实现
缓存工具封装 方法: java转json 将Java对象序列化为json并存储在String类型的key中,并且能够设置TTL过期时间
1 2 3 public void set (String key, Object value, Long time , TimeUnit unit) { stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit); }
方法: java转json,用于处理缓存击穿 将Java对象序列化为json并存储在String类型的key中,并且能够设置TTL过期时间,用于处理缓存击穿问题
1 2 3 4 5 6 7 8 public void setWithLogicalExpire (String key, Object value, Long time , TimeUnit unit) { RedisData redisData = new RedisData (); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now().plusSeconds(time)); stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); }
方法: 根据指定key查询缓存,并转为指定类型 根据指定key查询缓存,并转为指定类型,利用缓存空值的方式解决缓存穿透问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public <R,ID> R queryWithPassThrough ( String keyPre, ID id , Class<R> type , Function<ID , R> dbFallback, Long time , TimeUnit unit) { String key = keyPre + id; String Json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(Json)){ return JSONUtil.toBean(Json, type); } if (Json != null ){ return null ; } R r = dbFallback.apply(id); if (r == null ){ stringRedisTemplate.opsForValue().set(key,"" ,CACHE_NULL_TTL,TimeUnit.MINUTES); return null ; } this .set(key,r,time,unit); return r; }
2023.2.24 —– 优惠劵秒杀 全局唯一ID 问题描述:
当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:
id的规律性太明显
受单表数据量的限制
容易造成数据泄露的问题
全局ID生成器 它是一种在分布式系统下用来生成全局唯一id的工具,(也称分布式唯一id)。
特性: 唯一性、高性能、高可用、安全性、递增性
ID的自增: 不使用redis自增的数值,而是拼接一些其他的信息 :
ID的组成 :
符号位: 1bit ,永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Component public class RedisIdWorker { @Resource private StringRedisTemplate stringRedisTemplate; private static final long BEGIN_TIMESTAMP = 1620995200L ; private static final int COUNT_BITS = 32 ; public long nextId (String keyPre) { LocalDateTime now = LocalDateTime.now(); long nowSeconds = now.toEpochSecond(ZoneOffset.UTC); long timestamp = nowSeconds - BEGIN_TIMESTAMP; String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd" )); Long aLong = stringRedisTemplate.opsForValue().increment("icr:" + keyPre + ":" + date); return timestamp << COUNT_BITS | aLong; } }
全局ID的生成策略 : UUID、Redis自增、snowflake算法、数据库自增
Redis自增ID策略:每天一个key,方便统计订单量
ID构造是 时间戳 + 计数器
实现优惠卷秒杀下单 背景
实现 下单时需要判断两点:
1 2 3 4 5 6 7 8 9 10 11 //1. 提交优惠卷id //2. 查询优惠卷信息 //3. 判断秒杀是否开启 //否 返回异常, 结束 //4. 是,判断库存是否充足 //否 返回异常, 结束 //5. 是,扣减库存,创建订单,返回订单信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Service public class VoucherOrderServiceImpl extends ServiceImpl <VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private RedisIdWorker redisIdWorker; @Resource private ISeckillVoucherService seckillVoucherService; @Override public Result seckillVoucher (Long voucherId) { SeckillVoucher voucher = seckillVoucherService.getById(voucherId); if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail("秒杀未开始!" ); } if (voucher.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail("秒杀已经结束!" ); } Integer stock = voucher.getStock(); if (stock == 0 ){ return Result.fail("已经被抢完了!" ); } boolean update = seckillVoucherService.update().setSql("stock = stock - 1" ) .eq("voucher_id" , voucherId).update(); if (!update){ return Result.fail("库存不足!" ); } VoucherOrder order = new VoucherOrder (); long orderId = redisIdWorker.nextId("order" ); order.setId(orderId); Long UserId = UserHolder.getUser().getId(); order.setUserId(UserId); order.setVoucherId(voucherId); save(order); return Result.ok(orderId); } }
超卖问题 问题描述:
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
实现 乐观锁 和悲观锁!!!
乐观锁 乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种: 版本号法、CAS法…
1 2 3 4 boolean update = seckillVoucherService.update().setSql("stock = stock - 1" ) .eq("voucher_id" , voucherId).eq("stock" ,voucher.getStock()) .update()
一人一单 需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
提交优惠卷id
查询优惠卷信息
判断秒杀是否开启 //否 返回异常, 结束
是,判断库存是否充足 //否 返回异常, 结束
是,扣减库存,创建订单,返回订单信息
根据用户id和优惠卷id查询该用户是否下过单,如果下过直接返回异常,反之继续执行之前的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 @Service public class VoucherOrderServiceImpl extends ServiceImpl <VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private RedisIdWorker redisIdWorker; @Resource private ISeckillVoucherService seckillVoucherService; @Override public Result seckillVoucher (Long voucherId) { SeckillVoucher voucher = seckillVoucherService.getById(voucherId); if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail("秒杀未开始!" ); } if (voucher.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail("秒杀已经结束!" ); } Integer stock = voucher.getStock(); if (stock < 1 ){ return Result.fail("已经被抢完了!" ); } Long UserId = UserHolder.getUser().getId(); synchronized (UserId.toString().intern()){ IVoucherOrderService orderService = (IVoucherOrderService) AopContext.currentProxy(); return createVoucherOrder(voucherId); } } @Transactional public Result createVoucherOrder (Long voucherId) { Long UserId = UserHolder.getUser().getId(); Integer count = query().eq("user_id" , UserId).eq("voucher_id" , voucherId).count(); if (count > 0 ){ return Result.fail("该用户已经购买过了!" ); } boolean update = seckillVoucherService.update().setSql("stock = stock - 1" ) .eq("voucher_id" , voucherId).eq("stock" ,0 ) .update(); if (!update){ return Result.fail("库存不足!" ); } VoucherOrder order = new VoucherOrder (); long orderId = redisIdWorker.nextId("order" ); order.setId(orderId); order.setUserId(UserId); order.setVoucherId(voucherId); save(order); return Result.ok(voucherId); } }
2023.2.28—–实现秒杀业务出现问题 优惠卷秒杀持续ing…..
分布式锁 多线程状态下实现同步的锁。
之前我们设置的锁,只是相对于同一jvm下的,如果部署在集群模式下那么这种情况就会出现危险,还是会引起插麦问题,或者说超问题下单。所以这里就需要用到分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
功能
实现分布式锁 分布式锁的核心是多进程之间互斥
, 满足这一点,常见的有三种
获取锁 互斥,确保只有一个线程获取锁
1 2 3 4 SETNX lock thread1 EXPIRE lock 10
添加过期时间,防止服务延机导致服务挂了。
业务逻辑
先尝试获取锁,返回结果。失败返回false
成功就执行业务,最后释放锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class SimpleRedisLock implements ILock { 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:" ; @Override public boolean tryLock (long timeOutSec) { long value = Thread.currentThread().getId(); Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value + "" , timeOutSec, TimeUnit.MINUTES); return Boolean.TRUE.equals(aBoolean); } @Override public void unLock () { stringRedisTemplate.delete(KEY_PREFIX + name); } }
释放锁 1 2 3 4 5 6 7 8 9 @Override public void unLock () { stringRedisTemplate.delete(KEY_PREFIX + name); }
实现业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 SimpleRedisLock lock = new SimpleRedisLock ("order:" + UserId, stringRedisTemplate);boolean isLock = lock.tryLock(1200 );if (!isLock){ return Result.fail("不允许重复下单!" ); } try { IVoucherOrderService orderService = (IVoucherOrderService) AopContext.currentProxy(); return orderService.createVoucherOrder(voucherId); } finally { lock.unLock(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 @Override public Result seckillVoucher (Long voucherId) { SeckillVoucher voucher = seckillVoucherService.getById(voucherId); if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail("秒杀未开始!" ); } if (voucher.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail("秒杀已经结束!" ); } Integer stock = voucher.getStock(); if (stock < 1 ){ return Result.fail("已经被抢完了!" ); } Long UserId = UserHolder.getUser().getId(); SimpleRedisLock lock = new SimpleRedisLock ("order:" + UserId, stringRedisTemplate); boolean isLock = lock.tryLock(1200 ); if (!isLock){ return Result.fail("不允许重复下单!" ); } try { IVoucherOrderService orderService = (IVoucherOrderService) AopContext.currentProxy(); return orderService.createVoucherOrder(voucherId); } finally { lock.unLock(); } } @Transactional public Result createVoucherOrder (Long voucherId) { Long UserId = UserHolder.getUser().getId(); Integer count = query().eq("user_id" , UserId).eq("voucher_id" , voucherId).count(); if (count > 0 ){ return Result.fail("该用户已经购买过了!" ); } boolean update = seckillVoucherService.update().setSql("stock = stock - 1" ) .eq("voucher_id" , voucherId).eq("stock" ,0 ) .update(); if (!update){ return Result.fail("库存不足!" ); } VoucherOrder order = new VoucherOrder (); long orderId = redisIdWorker.nextId("order" ); order.setId(orderId); order.setUserId(UserId); order.setVoucherId(voucherId); save(order); return Result.ok(voucherId); }
分布式锁的改进
防止因为业务阻塞而引起的误删除其他人的锁。
改进方法: 在执行释放锁的时候需要再次确认是否是自己的锁
在获取锁时存入线程标示(可以用UUID表示)
在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
如果一致则释放锁 ; b. 如果不一致则不释放锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class SimpleRedisLock implements ILock { 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:" ; private static final String ID_PREFIX = UUID.randomUUID().toString(true )+"=" ; @Override public boolean tryLock (long timeOutSec) { String value = ID_PREFIX + Thread.currentThread().getId(); Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, value + "" , timeOutSec, TimeUnit.MINUTES); return Boolean.TRUE.equals(aBoolean); } @Override public void unLock () { String threadId = ID_PREFIX + Thread.currentThread().getId(); String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); if (id.equals(threadId)){ stringRedisTemplate.delete(KEY_PREFIX + name); } } }
基于Redis的分布式锁实现思路:
利用set nx ex获取锁,并设置过期时间,保存线程标示
释放锁时先判断线程标示是否与自己一致,一致则删除锁
特性:
利用set nx满足互斥性
利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
利用Redis集群保证高可用和高并发特性
之前实现的分布式锁存在的问题
总结问题 根据项目实现思路一步步排查,依旧出现无法秒杀的问题,以及一人一单的问题依旧无法准确实现
暂未解决!!!
2023.3.1 ——达人探店 实现发表文章及其照片
上传文件的请求
实现上传文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Slf4j @RestController @RequestMapping("upload") public class UploadController { @PostMapping("blog") public Result uploadImage (@RequestParam("file") MultipartFile image) { try { String originalFilename = image.getOriginalFilename(); String fileName = createNewFileName(originalFilename); image.transferTo(new File (SystemConstants.IMAGE_UPLOAD_DIR, fileName)); log.debug("文件上传成功,{}" , fileName); return Result.ok(fileName); } catch (IOException e) { throw new RuntimeException ("文件上传失败" , e); } } private String createNewFileName (String originalFilename) { String suffix = StrUtil.subAfter(originalFilename, "." , true ); String name = UUID.randomUUID().toString(); int hash = name.hashCode(); int d1 = hash & 0xF ; int d2 = (hash >> 4 ) & 0xF ; File dir = new File (SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}" , d1, d2)); if (!dir.exists()) { dir.mkdirs(); } return StrUtil.format("/blogs/{}/{}/{}.{}" , d1, d2, name, suffix); } }
对于保存文件的地址
1 2 image.transferTo(new File (SystemConstants.IMAGE_UPLOAD_DIR, fileName));
这个需要我们自己去修改成为本地的地址
1 public static final String IMAGE_UPLOAD_DIR = "N:\\hepre\\nginx-1.18.0\\html\\hmdp\\imgs\\" ;
然后就可以点击发布博客【接口: /blog
】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RestController @RequestMapping("/blog") public class BlogController { @Resource private IBlogService blogService; @Resource private IUserService userService; @PostMapping public Result saveBlog (@RequestBody Blog blog) { UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); blogService.save(blog); return Result.ok(blog.getId()); } }
查看文章 接口:【点击文章就是显示文章的id】
然后根据文章的id查询文章的详细信息,最后返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @RestController @RequestMapping("/blog") public class BlogController { @GetMapping("/hot") public Result queryHotBlog (@RequestParam(value = "current", defaultValue = "1") Integer current) { Page<Blog> page = blogService.query() .orderByDesc("liked" ) .page(new Page <>(current, SystemConstants.MAX_PAGE_SIZE)); List<Blog> records = page.getRecords(); records.forEach(blog ->{ Long userId = blog.getUserId(); User user = userService.getById(userId); blog.setName(user.getNickName()); blog.setIcon(user.getIcon()); }); return Result.ok(records); } @GetMapping("/{id}") public Result queryBlogById (@PathVariable("id") Long id) { return blogService.queryBlogById(id); } }
文章点赞功能实现(基于redis实现) 接口
需求分析
同一个用户只能点赞一次,再次点击则取消点赞
如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
实现步骤:
给Blog类中添加一个isLike字段,标示是否被当前用户点赞
修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
实现点赞功能 接口:
1 2 3 4 5 @PutMapping("/like/{id}") public Result likeBlog (@PathVariable("id") Long id) { return blogService.likeBlog(id); }
service层实现功能 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Override public Result likeBlog (Long id) { Long userId = UserHolder.getUser().getId(); String key = "blog:liked" + id; Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString()); if (BooleanUtil.isFalse(isMember)){ boolean isSuccess = update() .setSql("liked = liked + 1" ).eq("id" , id).update(); if (isSuccess){ stringRedisTemplate.opsForSet().add(key,userId.toString()); } } else { boolean isSuccess = update() .setSql("liked = liked - 1" ).eq("id" , id).update(); if (isSuccess){ stringRedisTemplate.opsForSet().remove(key,userId.toString()); } } return Result.ok(); }
点赞了就会出现红色,同时redis中就会保存点赞用户的id及其作品
当我们再次按点赞按钮的时候,它的点赞数就会减一,同时redis中保存的数据也会同步的删除
点赞排行榜功能 仿微信实现早点赞的先排在前面
需求:按照点赞时间先后排序,返回Top5的用户
功能实现
根据传入的blog的id查询出前几名的用户id
根据用户id查询出用户
再将user转换成为userDTO为了保护用户的信息安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public Result queryBlogLikes (Long id) { String key = "blog:liked" + id; Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0 , 4 ); if (top5 == null || top5.isEmpty()){ return Result.ok(Collections.emptyList()); } List<Long> userIds = top5.stream().map(Long::valueOf).collect(Collectors.toList()); String idStr = StrUtil.join("," , userIds); List<UserDTO> userDTOS = userService.query().in("id" , userIds).last("ORDER BY FIELD(id," + idStr + ")" ).list(). stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList()); return Result.ok(userDTOS); }
2023.3.5 —–关注 实现接口 :
实现关注和取关的接口,对于博主和关注者之间使用一张关联表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RestController @RequestMapping("/follow") public class FollowController { @Resource private IFollowService followService; @PutMapping("/{id}/{isFollow}") public Result follow (@PathVariable("id") Long followUserId ,@PathVariable("isFollow") boolean isFollow) { return followService.follow(followUserId,isFollow); } @GetMapping("/or/not/{id}") public Result isFollow (@PathVariable("id") Long followUserId) { return followService.isFollow(followUserId); } }
service层实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Service public class FollowServiceImpl extends ServiceImpl <FollowMapper, Follow> implements IFollowService { @Override public Result follow (Long followUserId, boolean isFollow) { Long userId = UserHolder.getUser().getId(); if (isFollow){ Follow follow = new Follow (); follow.setUserId(userId); follow.setFollowUserId(followUserId); save(follow); } else { remove(new QueryWrapper <Follow>().eq("user_id" ,userId).eq("follow_user_id" ,followUserId)); } return Result.ok(); } @Override public Result isFollow (Long followUserId) { Long userId = UserHolder.getUser().getId(); Integer count = query().eq("user_id" , userId).eq("follow_user_id" , followUserId).count(); return Result.ok(count > 0 ); } }
共同关注 点击用户头像查看个人主页
点击头像查看用户笔记实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping("/of/user") public Result queryBolgByUserId (@RequestParam(value = "current",defaultValue = "1") Integer current, @RequestParam("id") Long id) { Page<Blog> page = blogService.query().eq("user_id" , id).page(new Page <>(current, SystemConstants.MAX_PAGE_SIZE)); List<Blog> records = page.getRecords(); return Result.ok(records); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @GetMapping("/{id}") public Result queryUserById (@PathVariable("id") Long userId) { User user = userService.getById(userId); if (user == null ){ return Result.ok(); } UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); return Result.ok(userDTO); }
共同关注查看 需求:利用Redis中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。
两个用户的交集 ,也就是两个set集合的交集
首先将关注的用户存入Redis中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Service public class FollowServiceImpl extends ServiceImpl <FollowMapper, Follow> implements IFollowService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result follow (Long followUserId, boolean isFollow) { Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; if (isFollow){ Follow follow = new Follow (); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean isSecc = save(follow); if (isSecc){ stringRedisTemplate.opsForSet().add(key,followUserId.toString()); } } else { boolean isSucc = remove(new QueryWrapper <Follow>().eq("user_id" , userId).eq("follow_user_id" , followUserId)); if (isSucc){ stringRedisTemplate.opsForSet().remove(key,followUserId.toString()); } } return Result.ok(); } }
实现共同关注 实现接口:
1 2 3 4 5 6 7 8 9 @GetMapping("/common/{id}") public Result followCommons (@PathVariable("id") Long id) { return followService.followCommons(id); }
service层实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public Result followCommons (Long id) { Long userId = UserHolder.getUser().getId(); String key1 = "follows:" + userId; String key2 = "follows:" + id; Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2); if (intersect == null || intersect.isEmpty()){ return Result.ok(Collections.emptyList()); } List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); List<UserDTO> userDTOS = service.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class) ).collect(Collectors.toList()); return Result.ok(userDTOS); }
关注推送实现 关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
Feed流模式
拉模式
张三、李四、王五三个人假设都发了微博,赵六关注了李四和张三,当赵六刷新信息的时候。它的收件箱就会拉去他关注的人发的微博
然后再收件箱中对赵六关注的人发的微博按照时间戳进行排序,最终得到按照时间的微博
如果赵六关注的人比较多,那么拉去微博就会很慢,非常耗内存
读模式 将微博直接全部推送给每一个粉丝,然后获取
但是这样对于粉丝多的来说就会非常耗内存
推拉混合模式 推拉结合模式:也叫做读写混合,兼具推和拉两种模式的优点。
使用两种结合的方式,就可以完美解决两种冲突的问题
需求实现:
修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
1 2 3 4 @PostMapping public Result saveBlog (@RequestBody Blog blog) { return blogService.saveBlog(blog); }
service层实现
收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Override public Result saveBlog (Blog blog) { UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); boolean succ = save(blog); if (!succ){ return Result.fail("笔记保存失败!" ); } List<Follow> follows = followService.query().eq("follow_user_id" , user.getId()).list(); for (Follow follow : follows) { Long userId = follow.getUserId(); String key = "feeds:" + userId; stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis()); } return Result.ok(blog.getId()); }
查询收件箱数据时,可以实现分页查询
实现接口 :
两个重点:
每次查询完要分析出本次查询的最小时间戳 ,这将作为下一次查询的起始(最大值)
找到查询出的最小元素的个数,并将其跳过,从下一个开始查
2023.3.6—–实现用户签到 对于redis中的GEO数据结构
例题: 搜索天安门( 116.397904 39.909005 )附近10km内的所有火车站,并按照距离升序排序
实现方法:
官网文档:
查询的中心点由以下强制选项之一提供:
FROMMEMBER
:使用给定的现有在排序集中的位置。<member>
FROMLONLAT
:使用给定的和位置。<longitude>``<latitude>
查询的形状由以下强制选项之一提供:
BYRADIUS
:与GEORADIUS
类似,根据给定的圆形区域搜索。<radius>
BYBOX
:在轴对齐的矩形内搜索,由 和 确定。<height>``<width>
该命令可选择使用以下选项返回其他信息:
WITHDIST
:同时返回返回的项目与指定中心点的距离。距离以为半径或高度和宽度参数指定的相同单位返回。
WITHCOORD
:同时返回匹配项的经度和纬度。
WITHHASH
:还以 52 位无符号整数的形式返回项目的原始地理哈希编码排序集分数。这仅对低级黑客或调试有用,否则对一般用户兴趣不大。
默认情况下,匹配项返回时未排序。要对它们进行排序,请使用以下两个选项之一:
ASC
:相对于中心点,从最近到最远对返回的项目进行排序。
DESC
:相对于中心点,从最远到最近对返回的项目进行排序。
默认情况下,将返回所有匹配项。若要将结果限制为前 N 个匹配项,请使用 COUNT<count>
选项。 使用该选项时,一旦找到足够的匹配项,该命令就会返回。这意味着返回的结果可能不是最接近指定点的结果,但服务器为生成它们而投入的工作量要少得多。 如果未提供,该命令将执行与指定区域匹配的项目数成比例的工作并对其进行排序, 因此,即使只返回几个结果,使用非常小的选项查询非常大的区域也可能很慢。ANY``ANY``COUNT
GEO数据结构用于解决附近商户查询的问题,但是由于项目目前使用的数据都是虚假数据。对于真实业务的逻辑及其实现思路都不太清除,所以次模块未完成
用bitMap实现用户签到
bitMap的用法
实现用户签到
1 2 3 4 @PostMapping("/sign") public Result sign () { return userService.sign(); }
service层实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override public Result sign () { Long userId = UserHolder.getUser().getId(); LocalDateTime now = LocalDateTime.now(); String key = "sign:" + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM" )); int nowDayOfMonth = now.getDayOfMonth(); stringRedisTemplate.opsForValue().setBit(key,nowDayOfMonth - 1 ,true ); return Result.ok(); }
统计当前用户连续签到天数
1 2 3 4 @PostMapping("/sign/count") public Result signCount () { return userService.signCount(); }
service层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Override public Result signCount () { Long userId = UserHolder.getUser().getId(); LocalDateTime now = LocalDateTime.now(); String key = "sign:" + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM" )); int nowDayOfMonth = now.getDayOfMonth(); List<Long> result = stringRedisTemplate.opsForValue().bitField( key, BitFieldSubCommands.create() .get(BitFieldSubCommands.BitFieldType.unsigned(nowDayOfMonth)).valueAt(0 ) ); if (result == null || result.isEmpty()){ return Result.ok(0 ); } Long num = result.get(0 ); if (num == null || num == 0 ){ return Result.ok(0 ); } int count = 0 ; while (true ){ if ((num & 1 )==0 ){ break ; }else { count++; } num >>>= 1 ; } return Result.ok(count); }