Redis学习路线(9)—— Redis的场景使用

默认做好了其它的前提,只专注于Redis使用

一、短信登录

在没有Redis数据库时,我们会基于Session实现登录(利用令牌进行授权),是怎么实现的呢?

(一)基于Session的短信登录功能

1、发送短信验证码

(1)流程: 客户端提交手机号 》 校验手机号 》 生成验证码 》 保存验证码到session 》 发送验证码

说明
请求方式POST
请求路径/usr/code
请求参数phone
返回值void
// Result 为结果类,成员变量有success、errorMsg、data、total,方法有成功结果(携带数据【包含了List以及统计总数的数据】和不携带数据)和错误结果(返回错误信息)
@Override
public Result sendCode(String phone, HttpSession session) {//1、校验手机号,校验不通过,则返回错误信息;校验通过,则查询用户信息。if(RegexUtils.isPhoneIncalid(phone)) //static boolean RegexUtils.isPhoneIncalid(String phone),自定义格式校验工具return Result.error("手机号格式错误");//2、生成验证码(6位数字),使用自定义随机数生成工具String code = RandomUtils.randomNumbers(6);//3、保存验证码到sessionsession.setAttribute("verify", code);//4、发送验证码,模拟发送成功,业务上可以使用阿里云的短信服务进行处理log.debug("发送短信验证码成功,验证码:{}", code);return Result.ok();
}

2、短信验证码登录、注册

(1)流程: 客户端提交手机号和验证码 》 校验验证码 》 根据手机号查询用户 》 查询用户是否存在(若不存在则创建新用户存储到数据库) 》保存用户到session

说明
请求方式POST
请求路径/usr/login
请求参数phone, verify
返回值void
private static final String USER_NICK_NAME_PREFIX = "Coder_";@Autowired
private LoginMapper loginMapper;@Override
public Result login(String phone, String verify, HttpSession session) {//1、校验验证码,若校验通过,则根据手机号查询用户;若不成功则添加到数据库String code = (String)session.getAttribute("verify");if(code == null || !verify.equals(code)) {	//检验验证码的存在状态,可能发生过期或未获取验证码的情况return Result.error("验证码错误");}private User user;if((user = loginMapper.queryUserByPhone(phone)) == null){// 新增用户,需要初始化用户的信息有 手机号,随机昵称,因为用户是根据手机号进行登录的,昵称可以采用随机字符的方式进行注册,即使重复也没关系。user = new User(phone, USER_NICK_NAME_PREFIX + RandomUtils.randomString(10));loginMapper.addUserByPhone(user);}//2、保存用户信息到sessionsession.setAttribute("user", user);return Result.ok();
}

3、校验登录状态

(1)流程: 客户端请求并携带cookie 》 从session中获取用户 》 判断用户是否存在(若不存在则拦截) 》 保存到ThreadLocal 》放行处理

拦截器:LoginInterceptor.java

public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServeltResponse response, Object handler){//1、获取sessionHttpSession session = request.getSession();//2、获取session中的用户User user = (User)session.getAttribute("user");//3、判断用户是否存在if(user == null){// 拦截,返回401状态码response.setStatus(401);return false;}//4、保存用户信息到ThreadLocal// UserHolder 中 定义了一个 ThreadLocal 实例对象,使用 set(User) 即可保存信息,使用 get() 即可获取信息,使用 remove() 即可删除信息UserHolder.saveUser(user);//5、放行return true;}
}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServeltResponse response,  Object handler, Exception ex){//移除用户UserHolder.removeUser();}

配置类:MVCConfig.java

@Configuration
public class MVCConfig implements WebMvcConfigurer {@Resourceprivate SpringRedisTemplate springRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {List<String> excludeList = new ArrayList();list.add("/usr/sendCode");list.add("/usr/login");list.add("/usr/logout");list.add("/usr/currentUser");list.add("/shop/**");list.add("/shop-type/**");list.add("/blog/hot");.....registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(excludeList).order(1);}
}

此时项目出现的问题: 用户信息没有脱敏。

解决方案: 返回部分信息即可,可以创建响应数据类xxDTO进行用户信息存储。

集群的session共享问题: 多态Tomcat并不共享session空间,当请求切换到不同的tomcat服务时导致数据丢失的问题。

解决方案: Redis代替session。

(二)基于Redis的短信登录功能

1、发送短信验证码

(1)流程: 客户端提交手机号 》 校验手机号 》 生成验证码 》 保存验证码到redis 》 发送验证码

@Override
public Result sendCode(String phone) {if(RegexUtils.isPhoneIncalid(phone))return Result.error("手机号格式错误");String code = RandomUtils.randomNumbers(6);//使用 phone-code 结构保存,保存验证码5分钟,5分钟后失效stringRedisTemplate.opsForValue().set("phone:"+phone, code, 5, TimeUnit.MINUTES);log.debug("发送短信验证码成功,验证码:{}", code);return Result.ok();
}

2、短信验证码登录、注册

(1)流程: 客户端提交手机号和验证码 》 校验验证码 》 根据手机号查询用户 》 查询用户是否存在(若不存在则创建新用户存储到数据库) 》保存用户到Redis

说明
请求方式POST
请求路径/usr/login
请求参数phone, verify
返回值void
private static final String USER_NICK_NAME_PREFIX = "Coder_";@Autowired
private LoginMapper loginMapper;@Override
public Result login(String phone, String verify) {String code = stringRedisTemplate.opsForValue().get("phone:"+phone);if(code == null || !verify.equals(code)) {	//检验验证码的存在状态,可能发生过期或未获取验证码的情况return Result.error("验证码错误");}User user;if((user = loginMapper.queryUserByPhone(phone)) == null){user = new User(phone, USER_NICK_NAME_PREFIX + RandomUtils.randomString(10));loginMapper.addUserByPhone(user);}//获取UUID唯一标识String token = "token:"+ UUID.randomUUID().toString(true).replaceAll("-","");//以token-HashMap结构存储对象信息。Map<String, Object> map = JSONObject.parseObject(JSONObject.toJSONString(user), Map.class);map.replaceAll((k,v) -> v.toString());stringRedisTemplate.opsForHash().putAll(token, map);	//设置有效期RedisUtils.REDIS.expire(tokenKey, 30, TimeUnit.MINUTES);return Result.ok();
}

3、校验登录状态

(1)流程: 客户端请求并携带token 》 从redis中获取用户 》 判断用户是否存在(若不存在则拦截) 》 保存到ThreadLocal,更新有效期 》放行处理

拦截器:LoginInterceptor.java

public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServeltResponse response, Object handler){//1、获取tokenString token = request.getHeader("authorization");if(token == null){response.setStatus(401);return false;}//2、获取redis中的用户Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:"+token);//3、判断用户是否存在if(userMap.isEmpty()){// 拦截,返回401状态码response.setStatus(401);return false;}//4、将Map转为实体类User user = JSON.parseObject(JSON.toJSONString(userMap), User.class);//5、将用户信息存储到ThreadLocalUserHolder.saveUser(user);//6、刷新token活跃状态stringRedisTemplate.expire("login:token:"+token, 30, TimeUnit.MINUTES);//7、放行return true;}
}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServeltResponse response,  Object handler, Exception ex){//移除用户UserHolder.removeUser();}

解决登录状态刷新问题: 拦截器只有在访问需要校验的网页才会刷新用户活跃状态。

解决方案: 拦截器执行链。

设置两个拦截器。

  • 拦截器1: 拦截所有路径,并检查用户token的存在并刷新,一律放行。
  • 拦截器2: 拦截需要登录的路径,不存在则拦截,存在则继续。
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServeltResponse response, Object handler){if(UserHolder.getUser() == null){response.setStatus(401);return false;}//7、放行return true;}
}public class RefreshTokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServeltResponse response, Object handler){//1、获取tokenString token = request.getHeader("authorization");if(token == null){return true;}//2、获取redis中的用户Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:"+token);//3、判断用户是否存在if(userMap.isEmpty()){return true;}//4、将Map转为实体类User user = JSON.parseObject(JSON.toJSONString(userMap), User.class);//5、将用户信息存储到ThreadLocalUserHolder.saveUser(user);//6、刷新token活跃状态stringRedisTemplate.expire("login:token:"+token, 30, TimeUnit.MINUTES);//7、放行return true;}
}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServeltResponse response,  Object handler, Exception ex){//移除用户UserHolder.removeUser();}

MVC配置文件新增拦截器

registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);

二、商户查询缓存

(一)添加Redis缓存

1、缓存作用模型: 客户端 》 Redis(命中返回) 》 数据库(查询返回客户端并写入缓存)

2、根据id查询商铺缓存流程: 客户端提交商铺 id 》 从缓存里读取(命中则返回商铺信息) 》 根据id查询数据库(不存在则返回错误信息) 》 数据库写入Redis 》 返回商铺信息。

3、逻辑过期的实现

(1)封装逻辑过期类

@Data
@NoArgsConfiguration
@AllArgsConfiguration
public class RedisData {private Object data;private LocalDateTime dateTime;
}

(2)保存逻辑过期方法

public void saveShop2Redis(Long id, Long expireSeconds){//1、查询店铺数据Shop shop = shopMapper.queryShopById(id);//2、封装逻辑过期时间RedisData redisData = new RedisData(shop, LocalDateTime.now().plusSeconds(expireSeconds));//3、写入RedisstringRedisTemplate.opsForValue().set("lock:shop:"+id, JSON.toJSONString(redisData));
}

3、项目实现

//可以存储在一个专门的类中进行引用
private static final String SHOP_KEY = "program:shop:";@Autowired
private ShopMapper shopMapper;@Override
public Result queryShopById(Long id) {try{//1、查询缓存是否存在商铺信息String shopJSON = stringRedisTemplate.opsForValue().get(SHOP_KEY + id);//2、从缓存里读取(命中则返回商铺信息)if(!shopJSON.isEmpty()){//存在,则直接返回Shop shop = JSONObject.parseObject(shopJSON, Shop.class);// 设置 3h 的缓存时长,若无相关访问该数据,则删除缓存,可自行定义。stringRedisTemplate.expire(HOP_KEY + id, 3, TimeUnits.HOURS);return Result.ok(shop);}//3、实现缓存重建String lockKey = "lock:shop:"+id;boolean isLock = tryLock(lockKey);//判断是否获取成功if(!isLock){//失败则休眠后重试Thread.sleep(50);return queryWithMutex(id);}	//4、根据id查询数据库(不存在则返回错误信息) Shop shop = shopMapper.queryShopById(id);if(shop == null){stringRedisTemplate.opsForValue().set(SHOP_KEY + id, null);stringRedisTemplate.expire(SHOP_KEY + id, 2, TimeUnits.MINUTES);return Result.error("未查询到该商铺");}//5、数据库写入RedisMap<String, Object> map = JSONObject.parseObject(JSONObject.toJSONString(shop), Map.class);map.replaceAll((k,v) -> v.toString());stringRedisTemplate.opsForHash().putAll(SHOP_KEY + id, map);	stringRedisTemplate.expire(HOP_KEY + id, 3, TimeUnits.HOURS);}catch(InteruptedException e) {throw new RuntimeException(e);}finally {unlock(lockKey);}//6、返回信息return Result.ok(shop);
}private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.SECONDS);return flag != null? flag : false;
}private void unlock(String key) {stringRedisTemplate.delete(key);
}

5、基于逻辑过期方式解决缓存击穿问题(仅改动代码)


public Result queryWithLogicalExpire(Long id){try{// 判断缓存中是否有对象过程....RedisData redisData = JSONObject.parseObject(shop, RedisData.class);Object data = redisData.getData();Shop shop = (Shop)data;//1、命中缓存//判断过期if(redisData.getDateTime < LocalDateTime.now()){//未过期返回店铺信息return Result.ok(shop);}//已过期则需要缓存重建//获取互斥锁String lockKey = "lock:shop:"+id;boolean isLock = tryLock(lockKey);//判断是否获取成功if(!isLock){//失败则休眠后重试Thread.sleep(50);return queryWithMutex(id);}//开启独立线程Executors.newFixedThreadPool(10).submit(() -> {this.saveShop2Redis(id, 30);});//返回店铺信息return shop;}catch(InteruptedException e) {throw new RuntimeException(e);}finally {unlock(lockKey);}return Result.ok(shop);
}

三、优惠券秒杀

(一)抢购秒杀优惠券初级版

说明
请求方式POST
请求路径/voucher-order/seckill/{id}
请求参数id,优惠券id
返回值订单id

1、需要注意的两个要点:

  • 秒杀活动只有在规定时间段内可以下单
  • 库存不足无法下单

2、流程: 客户端提交优惠券id 》 查询优惠券信息 》 判断秒杀是否开始(若未开始则返回错误) 》 判断库存是否充足(不足则返回错误) 》 扣除库存 》 创建订单 》 返回订单id

3、实现功能

@Resource
private SeckillVoucherMapper seckillVoucherMapper;@Resource
private RedisIdBuilder redisIdBuilder;@Override	
@Transactional	//添加事务
public Result seckillVoucher(Long voucherId) {//1、查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);//2、查询活动时间Long begin = voucher.getBeginTime();Long end= voucher.getEndTime();Long now = LocalDateTime.now();if(voucher.getBeginTime().isAfter(now)) return Result.error("秒杀活动还未开始");else if(voucher.getEndTime().isBefore(now)) return Result.error("秒杀活动已结束");//3、判断库存状态if( voucher.getStock() < 1 )return Result.error("优惠券已告罄");//4、扣减库存, update seckillvoucher set stock = stock - 1 where voucher_id = #{voucherId}boolean flag = seckillVoucherMapper.removeOneStock(voucherId);if(flag){return Result.error("库存不足");}//5、创建订单Long orderId = redisIdBuilder.nextId("order");orderMapper.createOrder(orderId, UserHolder.getUser().getId(), voucherId);//6、返回订单Idreturn Result.ok(orderId);
}

(二)超卖问题

当多用户同时进行秒杀活动,就会有超卖问题。

乐观锁解决超卖实现(改动代码)

// 要改变的地方有: 查询库存的信息保存,扣除库存时的判断//查询库存
int stock = voucher.getStock();if( stock < 1 )return Result.error("优惠券已告罄");//4、扣减库存, update seckillvoucher set stock = stock - 1 where voucher_id = #{voucherId} and stock = #{stock}
if(seckillVoucherMapper.removeOneStock(voucherId, stock)){return Result.error("库存不足");
}

乐观锁的一个缺点: 成功率太低,当一个第一个线程先查询到了库存,并且执行了减库存操作,但后续的线程在第一次查询是也查询到了库存,现在由于第一个线程完成了操作,库存不一致了,那么这次请求就失败了,在这段期间内的所有线程都会失败。

如何解决这个问题? 只要把条件放开,条件无需符合相等的原则,只需要完成正常库存判断即可。

if( voucher.getStock() < 1 )return Result.error("优惠券已告罄");//4、扣减库存, update seckillvoucher set stock = stock - 1 where voucher_id = #{voucherId} and stock > 0
if(seckillVoucherMapper.removeOneStock(voucherId)){return Result.error("库存不足");
}

(三)一人一单

1、需求: 修改秒杀业务,要求同一个优惠券,一个用户只能下一单。

2、增加的流程: 优惠券id和用户id查询订单,若不存在则允许减库存,若存在,则返回错误

3、实现

	//1、查询订单(通过优惠券id,用户id)Order order = orderMapper.queryByVoucherAndUser(UserHolder.getUser().getId(), voucherId);//2、判断是否存在if(Objects.nunNull(order){//存在则返回错误return Result.error("对不起您已经领过该优惠券");}//3、不存在,则减库存seckillVoucherMapper.removeOneStock(voucherId);//4、创建订单Long orderId = redisIdBuilder.nextId("order");orderMapper.createOrder(orderId, UserHolder.getUser().getId(), voucherId);

4、出现的一人超卖问题: 由于用户可以多次发起请求,每次发起请求都会被响应,多次请求又都查询出没有订单信息,所以都会往下继续执行减库存,创订单的操作。

解决方案: 加锁,这次加悲观锁,因为要锁住一个线程只执行一次。

public Result seckillVoucher(Long voucherId) {//前期工作...sychronized(UserHolder.getUser().getId().toString().intern()) {//获取事务代理对象,需要添加一个依赖aspectjweaver,启动类开启依赖@EnableAspectJAutoProxy(exposeProxy = true)SeckillVoucherService proxy = (SeckillVoucherService) AopContext.currentProxy();//在释放锁时,数据可以确保已经完成提交returen proxy.createVoucherOrder(voucherId);}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {Order order = orderMapper.queryByVoucherAndUser(UserHolder.getUser().getId(), voucherId);if(Objects.nunNull(order){return Result.error("对不起您已经领过该优惠券");}seckillVoucherMapper.removeOneStock(voucherId);Long orderId = redisIdBuilder.nextId("order");orderMapper.createOrder(new Order(orderId, UserHolder.getUser().getId(), voucherId));return Result.ok(voucherId);
}

5、集群情况下的并发问题: 同一个用户在集群环境下多次请求,同时抢购一个优惠券,会出现同步锁失效的情况。

(1)出现这种并发安全问题的原因: JVM内部维护了一个锁监视器,在同一个userid下,认为这个线程是同一个线程,但是当有两个或更多的JVM集群出来,而锁监视器并没有锁定同一个线程,所以才会有并发安全问题。

(2)解决方案: 分布式锁

(3)项目中替换同步锁

声明StringRedisTemplate

@Resource
private StringRedisTemplate redisTemplate;

使用Redis分布式锁

//1、创建锁对象
SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + UserHolder.getUser().getId(), redisTemplate);//2、获取锁
boolean isLock = lock.tryLock(1200);//3、判断是否获取成功
if(!isLock){//获取失败,返回错误或重试return Result.error("不允许重复下单");
}try{SeckillVoucherService proxy = (SeckillVoucherService) AopContext.currentProxy();//在释放锁时,数据可以确保已经完成提交returen proxy.createVoucherOrder(voucherId);
}finally{lock.unlock();
}

6、业务阻塞导致锁超时释放的问题

有两个线程,线程a 和 线程b ,线程a 获得锁开始执行自己的业务,但由于某种原因导致业务阻塞,线程a一直在等待业务的完成。

由于Redis锁设置了释放时间,线程a的锁在阻塞中已经被释放,当业务完成后,线程a 依旧执行释放锁操作,导致 线程b 获取的锁被释放,从而导致线程安全问题。

(1)原因: 线程被阻塞,分布式锁超时被释放,导致线程运行混乱。

(2)解决方法: 在业务完成后,先检查锁的标识是否一致,再判断是否释放锁。

(3)改进Redis分布式锁

获取锁时,存入线程表示

	private static final String UID = UUID.randomUUID().toString(true).replaceAll("-","");@Overridepublic boolean tryLock(long timeoutSec) {//1、获取线程表示String threadId = UID + Thread.currentThread().getId();//2、获取锁+存储线程标识Boolean absent = redisTemplate.opsForValue().setIfAbsent(LOCK + threadName, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(absent);}@Overridepublic void unlock() {//1、判断所表示是否一致if(redisTemplate.opsForValue().get(LOCK + threadName).equals(UID + Thread.currentThread().getId())){redisTemplate.delete(LOCK + threadName);}}

7、由于JVM的垃圾回收机制,线程在释放锁之前可能会遭遇阻塞,造成超时释放锁

(1)解决方法: 将判断表示与释放锁形成原子性。

(2)实现方法: 使用Lua脚本,编写多条Redis,保证Redis命令的原子性。

Lua脚本的使用方法: Redis提供了一个回调函数,可以调用脚本。

EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose

(3)释放锁的业务流程

  • 获取锁中的线程标识
  • 判断是否与指定标识(当前线程标识)一致
  • 如果一致则释放锁(删除)
  • 如果不一致则什么都不做

(4)Lua脚本实现

-- 判断是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then-- 释放锁return redis.call('del', KEYS[1])
end
return 0

(5)Java执行Lua脚本

RedisTemplate调用Lua的API源码

@Override
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {return scriptExecutor.execute(script, keys, args);
}

调用Lua脚本实现原子性操作

	private static final DefaultRedisScript UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}   @Overridepublic void unlock() {redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(LOCK + threadName), UID + Thread.currentThread().getId());}

(六)Redis优化秒杀

Redis结构1:key=店铺id:优惠券id,value=用户id,存储某个用户抢购了某个店铺的某个优惠券的信息,并设置TTL
Redis结构2:key=店铺id:优惠券id,value=数量,存储某个店铺的某个优惠券的库存信息,并设置TTL

1、优化流程: 某个用户抢购某个店铺的某个优惠券,提交店铺id 与 优惠券id 》 查询Redis关于shopid:voulerid 对应的 field(查询到用户信息,直接返回错误信息) 》保存抢购信息到redis同时在相应的库存信息自减,再开一个异步线程生成订单写入数据库,并返回结果。

2、实现(改动部分)

优化点:

  • (1)只关注于Redis缓存的操作,主线程不掺杂数据库操作
  • (2)使用线程池操作数据库生成订单,减少了主程序对数据库的操作事件,提升工作性能
  • (3)使用Redis这样的快速响应数据库,提升工作性能,可以制作集群提高请求载荷
//1、Redis查询库存
String stock = redisTemplate.opsForValue().get(key);
if (Objects.isNull(stock)){return Result.error("活动未开始或以结束");
}
if (Integer.parseInt(stock) < 1){return Result.error("库存不足");
}//2、查询缓存中是否有该用户
Boolean isMember = redisTemplate.opsForSet().isMember(key, uid);
if (Boolean.TRUE.equals(isMember)){//若存在return Result.error("您已经买过了");
}//若不存在则存储抢购信息,减库存并返回正确信息,并设置过期时间(即活动结束时长)
redisTemplate.opsForSet().add(key, uid);        
redisTemplate.expire(key, realseTime, TimeUnit.HOURS);
redisTemplate.opsForValue().increment("stock:"+key, -1);//3、开启线程执行常见订单操作,采用的是线程池
POOL.submit(() -> {mapper.createOrderByVouler(redisIdBuilder.nextId("order"), shopId, voulerId, uid);
});return Result.ok(voulerId);

3、优化项目,实现异步秒杀

(1)通过命令行模式实现创建队列(也可以在脚本中 先判断指定消费者组和队列是否存在,再决定是否进行创建)

#创建消费者组同时创建队列
XGROUP CREATE stream.oreders g1 0 MKSTREAM

(2)改写lua脚本(改写部分)

local orderId = ARGV[3];#发送消息到队列
redis.call("XADD", "stream.orders", "*" , "userId", userid, "voucherId", voucherId, :"id", orderId);

(3)服务实现改写:

//抽取订单创建的功能代码(因为消息队列为该功能分了功能角色,消费者负责检验数据与发送消息,生产者读取消息进行后续工作)
pool.submit(() -> {String queueName = "stream.orders";@Overridepublic void run(){try{while(true){//1、获取消息队列中的订单消息List<MapRecord<String, Object, Object>> list = redisTemplate.opsForStream().read(Consumer.form("g1","c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create(queueName, ReadOffset.lostConsumer()));//2、判断是否获取成功if(Objects.isNull(list) || list.isEmpty()){//若失败,则继续下一次循环continue;}//若成功,则解析订单信息MapRecord<String, Object, Object> record= list.get(0);Map<Object, Object> values= map.getValue();Order order = JSONUtils.toJsonObject(JSONUtils.toJsonStr(values), Order.class);//2、创建订单mapper.createOrderByVouler(order);//3、ACK确认,队列名 + 消费者组ID + 消息IDredisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());}} catch (Exception e){log.error("处理订单异常:{}", e);handlePenddingList();}}
});//队列异常处理的方法
private void handlePenddingList() {try {while(true){//1、获取消息队列中的订单消息List<MapRecord<String, Object, Object>> list = redisTemplate.opsForStream().read(Consumer.form("g1","c1"),StreamReadOptions.empty().count(1)),StreamOffset.create(queueName, ReadOffset.from("0")));//2、判断是否获取成功if(Objects.isNull(list) || list.isEmpty()){//若失败,则结束break;}//若成功,则解析订单信息MapRecord<String, Object, Object> record= list.get(0);Map<Object, Object> values= map.getValue();Order order = JSONUtils.toJsonObject(JSONUtils.toJsonStr(values), Order.class);//2、创建订单mapper.createOrderByVouler(order);//3、ACK确认,队列名 + 消费者组ID + 消息IDredisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());}} catch(Exception e){log.error("pedding-list异常:{}", e);Thread.sleep(20);}
}

四、附近的商户

(一)需求:

  • 1、通过用户授权获取用户位置
  • 2、通过Redis的GEO结构,计算出直线距离并排序
  • 3、通过商铺ID获取商铺列表

(二)实现:

private void loadShopData(){//1、查询所有店铺List<Shop> list = shopMapper.getAllShop();//2、按照typeId分组Map<Long. List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop:getTypeId));//3、分批写入Redisfor (Map.Entry<Long, List<Shop>> entry : map.entrySet()){Long typeID = entry.getKey();String key = "shop:geo:" + typeID;List<Shop> value = entry.getValue();List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());for(Shop shop){//redisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY), shop.getId().toString());location.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(), new Point(shop.getX(), shop.getY())));}redisTemplate.opsForGeo().add(key, locations);}
}

查询附近的商铺

private Result queryShopByType(Integer typeId, Integer current, Double x, Double y){//1、判断是否需要根据坐标查询if(x == null || y == null){return Result.ok(shopMapper.getAllShopByType(typeId));}//2、查询redis,limit是查询数量GeoResults<RedisGeoCommands.GeoLocation<String>> results = GredisTemplate.opsForGeo().search( "shop:geo:" + typeId, GeoReference.fromCoordinate(x, y), new Distance(5000), RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(15));//3、解析idif(results == null){return Result.ok(Collections.emptyList());}List<GeoResults<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();List<Long> ids = new ArrayList<>(list.size());Map<String, Distance) distanceMap = new HashMap<>(list.size());list.stream().forEach(result -> {String shopIdStr = result.getContent().getName();ids.add(Long.valueOf(shopIdStr));Distance distance = result.getDistance();distanceMap.put(shopIdStr, distance);})//4、根据ID 查询 ShopString[] idsStr= ids.toArray(new String[0]);List<Shop> shops = shopMapper.getAllShopByIds(idsStr);for (Shop shop: shops){shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());}return shops;
}

五、UV统计

UV统计,主要是通过使用Redis的HyperLogLog来记录用户访问数。

private Result getTotalClickCount(){String[] values = new String[1000];for(int i = 0; i < 1000000; i++) {i = i % 1000;values[i] = "user_" + i;if(i === 999){redisTemplate.opsForHyperLogLog().add("h12",values);}}long count = redisTemplate.opsForHyperLogLog().size("h12");}

六、用户签到

(一)签到实现

private Result sign(){//获取当前登录用户Long userId = UserHolder.getUser().getId();//获取日期LocalDateTime now = LocalDateTime.now();//拼接keyString keySuffix = non.format(DateTimeFormatter.ofPattern(":yyyyMM"));String key = "sign:" + userId + keySuffix;//获取今天是本月的第几天int dayOfMounth = now.getDayOfMounth();//写入RedisredisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);return Result.ok();
}

(二)连续签到

private Result countOfSign(){Long userId = UserHolder.getUser().getId();LocalDateTime now = LocalDateTime.now();String keySuffix = non.format(DateTimeFormatter.ofPattern(":yyyyMM"));String key = "sign:" + userId + keySuffix;int dayOfMounth = now.getDayOfMounth();//1、获取本月到今日为止的记录redisTemplate.opsForValue().bitField(key,BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));if(result == null | result.isEmpty())return Result.ok(0);long num = result.get(0);if(num == null | num == 0)return Result.ok();int count = 0;while(true){if(num%1)==0){break;}else{count++;}num >>>= 1;}
}
return Result.ok(count);

七、好友关注

(一)关注和取关

1、查询是否关注用户

private Result isFollow(Long followUserId) {//	1、获取登录用户Long currentUser= UserHolder.getUser().getId();String prefix = "follow:auth:";if(Objects.isNull(currentUser))return Result.error("用户未登录");// 2、查询关注列表, select count(*) from follow_list where follow_user = #{followUserId} and user_id = #{userId}Boolean flag = redisTemplate.opsForSet().isMember(prefix + followUserId, currentUser);if(Objects.isNull(flag))return Result.ok(true);return Result.ok(false);
}

2、关注和取关接口

private Result follow(Long followUserId, Boolean isFollow) {//	获取登录用户Long currentUser= UserHolder.getUser().getId();String prefix = "follow:auth:";//1、判断关注状态switch(isFollow){case true://2、关注,新建数据Follow follow = new Follow();follow.setUserId(currentUser);follow.setFollowUserId(followUserId);//insert into follow_list (xx,xx,xx, follow_user_id, user_id) value (#{xx}, #{xx}, #{xx}, #{followUserId}, #{userId})followMapper.createFollow(follow);//存储到RedisredisTemplate.opsForSet().add(prefix + followUserId, currentUser);break;case false://3、取关,删除,delete follow_list where user_id = #{userId} and follow_user_id = #{followUserId}redisTemplate.opsForSet().remove(prefix + followUserId, currentUser);break:default:return Result.error("请求错误");}//4、返回return Result.ok();
}

(二)共同关注

private Result CommonFollow (long queryUser){//1、获取当前用户long currentUser = UserHolder.getUser().getId();//2、获取两个用户的关注列表,并存储在Redis中String prefix = "follow:auth:";Set<String> unionSet = redisTemplate.opsForSet().intersect(prefix + queryUser, prefix + currentUser);if(unionSet == null || unionSet.isEmpty())return Result.ok(Collections.emptyList());List<Long> ids = unionSet.stream().map(Long::valueOf).collect(Colletors.toList());List<User> users = userService.listById(ids).stream().map(user -> JSONObject.parseObject(JSONObject.toJSONString(user), UserDTO.class).collect(Collectors.toList()));return users;
}

(三)关注推送Feed

1、概念: 通过无限拉取刷新获取新的信息。

2、模式:

  • Timeline: 不做内容筛序,简单的按内容发布时间排序,常用于好友或关注。
    • 优点: 信息全面,不会缺失,并且实现简单。
    • 缺点: 信息噪声较多,用户不一定感兴趣,内容获取效率低。
  • 智能排序: 利用智能算法屏蔽掉违规的、用户不感兴趣的内容,推送用户感兴趣信息来吸引用户。
    • 优点: 推送用户感兴趣信息,用户粘度性高,容易沉迷。
    • 缺点: 如果算法不精准,可能会起反作用。

3、Feed流的实现方案

(1)拉模式: 也叫做读扩散。

流程: 每一位博主都有一个内容队列,每发一次博文,内容队列增加一条BlogID,每个用户都有一个收件队列,当用户关注一个博主时,会拉取当前时间之后的所有博文,而不会获取全部博文。

(2)推模式: 也叫做写扩散(常用)。

流程: 每一位博主拉取自己的关注列表,每次发送博文,会推送到关注者收件队列,用户订阅到推送信息,则开始读取。

(3)推拉结合模式: 也叫做读写混合,兼具推和拉模式的优点。

流程: 每一位博主都有自己的活跃关注用户和普通关注用户,使用推模式发送给活跃用户,使用拉模式发送给普通用户。

4、实现推模式实现关注推送

(1)需求:

  • 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
  • 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
  • 查询收件箱数据时,可以实现分页查询

(2)修改新增探店笔记的业务:

private Result saveBlog(Blog blog){//1、获取用户long currentUser = UserHolder.getUser().getId();blog.setUserId(currentUser);//2、保存探店博文boolean isSuccess = blogService.save(blog);if(!isSuccess)return Result.error("新增失败");//3、查询笔记作者的所有粉丝, select * from follow_list where follow_user_id = #{userId}List<Follow> follows = followMapper.getAllFollows(currentUser);//4、推送笔记id给所有粉丝for (Follow follow: follows) {// 获取粉丝idLong userId = follow.getUserId();// 推送String key = "feed:" + currentUser;redisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());}//3、返回博文idreturn Result.ok(blog.getId());
}

(3)获取关注者新发的博文

private Result getFollowMessage(long currentTime, long bloggerID, int offset, int max){//1、获取当前用户Long userId = UserHolder.getUser().getId();//2、查询收件箱String key = "feed:" + userId;Set<ZSetOperations.TypedTuple<String>> typedTuples = redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);if(typedTuples == null || typedTuples.isEmpty()){return Result.ok();}//3、解析数据:blogid,minTime,offsetList<Long> ids = new ArrayList<>(typedTuples.size());long minTime = 0;int ofs = 1;for(ZSetOperations.TypedTuple<String> tuple: typedTuples){// 获取idids.add(Long.alueOf(tuple.getValue()));//	获取分数long time = tuple.getScore().longValue();if(time == minTime){ofs++;}else{minTime = time;ofs = 1;}}//4、根据id查询blogStringBuilder builder = new StringBuilder();for(Long id: ids){builder.append(id);}List<Blog> blogs = blogMapper.queryBlogsById(ids, builder.toString());//查询blog是否被点赞过for (Blog blog : blogs){queryBlogUser(blog);isBlogLiked(blog);}ScrollResult r = new ScrollResult();r.setList(blogs);r.setOffset(ofs);r.setMinTime(minTime);return Result.ok();
}

八、达人探店

(一)发布探店笔记

1、数据的准备: 日志,也就是log,一个日志发布功能需要有 图片、以及分享文字。

2、数据的处理:

(1)图片的处理: 从前台获取的图片信息,经过原始文件名 》 生成存储文件名 》 保存图片 的流程就可以保存图片,并传回存储的文件名进行回显。

public Result uploadImage(MultipartFile image) {try{//	1、获取原始文件名String originalFilename = image.getOriginalFilename();//	2、生成新文件名String fileName = createNewFileName(originalFilename);//	3、保存文件,保存地址是本机地址,一般会存在服务器上image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));//	4、返回结果log.debug("文件上传成功,{}", fileName);return Result.ok(fileName);} catch (IOException e) {throw new RuntimeException("文件上传失败", e);}
}

(2)保存博文: 通过前台获取的图片的filename, 博文内容conent,以及用户id组合起来的Blog类,只需要存储这个博文,并返回博文ID进行回显。

public Result saveBlog(Blog blog) {//1、获取用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId);//2、保存探店博文blogService.save(blog);//3、返回博文idreturn Result.ok(blog.getId());
}

(3)查看博文: 通过前台获取blogid,进行查询,返回笔记信息以及发布的用户信息

public Result queryBlogById(Long id) {//1、Blog信息Blog blog = blogMapper.getById(id);if(Objects.isNull(blog)){return.error("博文不存在");}//2、查询blog相关用户Long userId = blog.getUserId();User user = userMapper.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());//3、返回博文信息return Result.ok(blog);
}

(二)点赞: 通过前台获取blogid,userid,查询相关blog,并自增点赞数

1、需求:

  • 同一个用户只能点赞一次Blog,再次点击则取消点赞。
  • 若当前用户点赞,则高亮。

2、解决方案: 使用Redis的ZSET结构,该结构唯一且有序,将该BLOG点赞过的用户进行一次查询并保存到Redis中,当一个用户点赞过,就加入到缓存,再一次点赞,就删除相应缓存。

3、实现:

public Result likeBlog(Long id){//1、获取当前用户Long userID = UserHolder.getUser().getId;//2、判断当前用户是否已经点赞String key = "blog:liked:" + id;Boolean isMember = Objets.isNull(redis.Template.opsForZSet().score(key, userId.toString()));//3、若未点赞则点赞if(Boolean.isFalse(isMember)){//数据库点赞+1,update from blog set like = like + 1 where id = #{id}boolean isSuccess = blogMapper.plusBlogLike(id);//保存用户到Redis的Set集合if(isSuccess){redisTemplate.opsForZSet().add(key, userId.toString(), LocalDateTime.now());}} else {//4、若已点赞则取消点赞//数据库点赞-1,update from blog set like = like - 1 where id = #{id}boolean isSuccess = blogMapper.minusBlogLike(id);//Redis集合删除if(isSuccess){redisTemplate.del(key);}}return Result.ok();
}

(三)点赞排行榜: 通过前端获取blogid,查询相关用户

1、实现: Redis的命令: ZRANGE z1 0 4,意思是sorted_set 查询一个索引范围内的值,因为存储时就是按照时间存储的,所以在redis中是升序排序,若想要获取最早的几个用户,就要用到ZRANGE指令,若想获取最新的用户则使用ZREVRANGE

private Result getLikeUserTOP(int count, long blogId){String key = "blog:liked:";//1、从redis获取前n个用户Set<String> userList = redisTemplate.opsForZSet().range(key + id, 0, count);if(Objects.isNull(userList) || userList.isEmpty())return Result.error(Collections.emptyList());//2、解析用户idList<Long> ids = userList.stream().map(Long::valueOf).collect(Colletors.toList());//3、查询用户List<User> users = userService.listById(ids).stream().map(user -> JSONObject.parseObject(JSONObject.toJSONString(user), UserDTO.class).collect(Collectors.toList()));//4、返回return Result.ok(users);
}

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

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

相关文章

现代C++中的从头开始深度学习:激活函数

一、说明 让我们通过在C中实现激活函数来获得乐趣。人工神经网络是生物启发模型的一个例子。在人工神经网络中&#xff0c;称为神经元的处理单元被分组在计算层中&#xff0c;通常用于执行模式识别任务。 在这个模型中&#xff0c;我们通常更喜欢控制每一层的输出以服从一些约束…

Java框架(九)--Spring Boot入门(1)

SpringBoot 2.x入门简介 学前基础 Maven Spring MVC理念 开发环境 Spring Boot官网版本介绍 https://spring.io/projects/spring-boot#learn 我们点击 Reference Doc. &#xff0c;再点击Getting Started&#xff0c;就可以看到官网系统环境说明了 官网系统环境说明 Sp…

MongoDB教程-7

正如在MongoDB关系的最后一章中所看到的&#xff0c;为了在MongoDB中实现规范化的数据库结构&#xff0c;我们使用了引用关系的概念&#xff0c;也被称为手动引用&#xff0c;在这个概念中&#xff0c;我们手动将被引用文档的id存储在其他文档中。然而&#xff0c;在一个文档包…

LangChain+ChatGLM大模型应用落地实践(一)

LLMs的落地框架&#xff08;LangChain&#xff09;&#xff0c;给LLMs套上一层盔甲&#xff0c;快速构建自己的新一代人工智能产品。 一、简介二、LangChain源码三、租用云服务器实例四、部署实例 一、简介 LangChain是一个近期非常活跃的开源代码库&#xff0c;目前也还在快速…

使用vscode进行远程开发服务器配置

1.下载vscode 2.给vscode 安装python 和 remote ssh插件 remote—SSH扩展允许您使用任何具有SSH服务器的远程机器作为您的开发环境。 3.安装remote-SSH插件之后&#xff0c;vscode左侧出现电脑图标&#xff0c;即为远程服务&#xff0c;按图依次点击&#xff0c;进行服务器配置…

运维:18工作中常用 Shell 脚本, 强烈推荐

1、检测两台服务器指定目录下的文件一致性 #!/bin/bash ###################################### 检测两台服务器指定目录下的文件一致性 ##################################### #通过对比两台服务器上文件的md5值,达到检测一致性的目的 dir=/data/web b_ip=192…

访问者模式——操作复杂对象结构

1、简介 1.1、概述 访问者模式是一种较为复杂的行为型设计模式&#xff0c;它包含访问者和被访问元素两个主要组成部分。这些被访问的元素通常具有不同的类型&#xff0c;且不同的访问者可以对它们进行不同的访问操作。访问者模式使得用户可以在不修改现有系统的情况下扩展系…

TensorRT学习笔记--基于TensorRT部署YoloV3, YoloV5和YoloV8

目录 1--完整项目 2--模型转换 3--编译项目 4--序列化模型 5--推理测试 1--完整项目 以下以 YoloV8 为例进行图片和视频的推理&#xff0c;完整项目地址如下&#xff1a;https://github.com/liujf69/TensorRT-Demo git clone https://github.com/liujf69/TensorRT-Demo.…

Open3D点云数据处理(十八):最小二乘直线拟合(二维)

文章目录 1 最小二乘直线拟合原理2 最小二乘直线拟合代码实现3 点云最小二乘直线拟合专栏目录:Open3D点云数据处理(Python) 1 最小二乘直线拟合原理 最小二乘直线拟合是一种常见的数据拟合方法,通常用于拟合一组二元数据点的直线方程。其原理是通过最小化数据点到拟合直线…

PostgreSql 进程及内存结构

一、进程及内存架构 PostgreSQL 数据库运行时&#xff0c;使用如下命令可查询数据库进程&#xff0c;正对应上述结构图。 [postgreslocalhost ~]$ ps -ef|grep post postgres 8649 1 0 15:05 ? 00:00:00 /app/pg13/bin/postgres -D /data/pg13/data postgres …

一篇聊聊JVM优化:堆

一、Java 堆概念 1、简介 对于Java应用程序来说&#xff0c;Java堆&#xff08;Java Heap&#xff09;是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享 的一块内存区域&#xff0c;在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例&#xff0c;Java 世界…

【算法第十八天8.2】235. 二叉搜索树的最近公共祖先 701.二叉搜索树中的插入操作 450.删除二叉搜索树中的节点

链接力扣235. 二叉搜索树的最近公共祖先 思路 // 二叉搜索树的最近公共祖先&#xff0c;可以根据值判断 class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {// 如果p、q在左子树if (root.val > p.val && root.val…

MongoDB文档--架构体系

阿丹&#xff1a; 在开始学习先了解以及目标知识的架构体系。就能事半功倍。 架构体系 MongoDB的架构体系由以下几部分组成&#xff1a; 存储结构&#xff1a;MongoDB采用文档型存储结构&#xff0c;一个数据库包含多个集合&#xff0c;一个集合包含多个文档。存储形式&#…

js设计模式-常见的13种

在JavaScript中&#xff0c;有多种常见的设计模式可供使用。以下是13种常见的JavaScript设计模式&#xff1a; JavaScript设计模式 单例模式&#xff08;Singleton Pattern&#xff09;工厂模式&#xff08;Factory Pattern&#xff09;抽象工厂模式&#xff08;Abstract Fac…

Quartz使用文档,使用Quartz实现动态任务,Spring集成Quartz,Quartz集群部署,Quartz源码分析

文章目录 一、Quartz 基本介绍二、Quartz Java 编程1、文档2、引入依赖3、入门案例4、默认配置文件 三、Quartz 重要组件1、Quartz架构体系2、JobDetail3、Trigger&#xff08;1&#xff09;代码实例&#xff08;2&#xff09;SimpleTrigger&#xff08;3&#xff09;CalendarI…

python-Excel数据模型文档转为MySQL数据库建表语句(需要连接数据库)-工作小记

将指定Excel文档转为create table 建表语句。该脚本适用于单一且简单的建表语句 呈现效果 代码 # -*- coding:utf-8 -*- # Time : 2023/8/2 17:50 # Author: 水兵没月 # File : excel_2_mysql建表语句.py import reimport pandas as pd import mysql.connectordb 库名mydb m…

ELK高级搜索(一)

文章目录 ELK搜索1&#xff0e;简介1.1 内容1.2 面向 2&#xff0e;Elastic Stack2.1 简介2.2 特色2.3 组件介绍 3&#xff0e;Elasticsearch3.1 搜索是什么3.2 数据库搜索3.3 全文检索3.4 倒排索引3.5 Lucene3.6 Elasticsearch3.6.1 Elasticsearch的功能3.6.2 Elasticsearch使…

【算法题】2790. 长度递增组的最大数目

题目&#xff1a; 给你一个下标从 0 开始、长度为 n 的数组 usageLimits 。 你的任务是使用从 0 到 n - 1 的数字创建若干组&#xff0c;并确保每个数字 i 在 所有组 中使用的次数总共不超过 usageLimits[i] 次。此外&#xff0c;还必须满足以下条件&#xff1a; 每个组必须…

Python3 网络爬虫开发实战

JavaScript逆向爬虫 JavaScript接口加密技术&#xff0c;JavaScript有以下两个特点&#xff1a; JS代码运行在客户端&#xff0c;所以它必须在用户浏览器加载并运行JS代码公开透明&#xff0c;所以浏览器可以直接获取到正在运行的JS源码。 所以JS代码不安全&#xff0c;任何…

电脑安装新系统不知道去哪里下载,看我就够了

大家在日常生活中肯定都会遇到电脑安装系统的需求&#xff0c;如果去微软官方购买正版的系统又很贵&#xff0c;又不太想花这个冤枉钱&#xff0c;这个时候我们就不得不去网上查找一些免费好用的系统&#xff0c;可是百度一下&#xff0c;或者Google一下&#xff0c;各种下载系…