当前位置: 首页 > java >正文

黑马点评实战笔记

首先第一个拦截器负责用户请求刷新时刷新用户的登录状态,从请求头中获取token,根据token获取用户信息,如果用户信息存在,将其存储到UserHolder中,刷新token的有效期,不存在直接放行

第二个拦截器检查UserHolder中是否由用户信息,登录则放行

HttpClient

支持HTTP协议,通过HTTP协议建立透明的连接,创建HttpClient对象模拟用户登录,HttpPost构建post请求,StringEntity:将JSON数据作为请求体发送,HttpResponse接受服务器的响应,从数据库分页读取用户信息,使用每个用户的手机号调用登录接口,模拟用户登录,获取并存储token,解析登录接口返回的JSON,提取token

使用的是jmeter对秒杀接口进行压测,因为是高并发环境下需要测试库存
session与cookie的区别:

session是客户端与服务端建立的连接,存储在服务端,cookie在浏览器中

将用户存储到ThreadLocal中,可以保证每个线程都拥有该变量的独立副本,用户发送请求时,访问tomcat的端口,需要线程对这个端口进行监听,创建socket连接,监听线程会从tomcat的线程池中取出一个线程执行用户请求,socket处理客户端与服务端之间的通信,当tomcat的socket接收到客户端的请求数据后,监听线程会从tomcat的线程池中取出线程进行分析,处理请求完成后对这个程序进行分析,所以需要用ThreadLocal做到线程隔离

Client->tomcat (Thread) -->创建socket进行连接,Thread从tomcat线程池中拿出线程执行客户请求

处理session集群共享问题:

使用SpringMVC实现登录拦截功能:拦截器类和配置类

拦截器类检查用户是否登录,配置类配置路径:不需要用户登录就可以访问的公共资源,如登录页面、验证码获取

tomcat不共享session存储,造成切换到不同tomcat的数据丢失问题

phone+token作为key,value是user的信息,使用redis进行存储

这里设置token的TTL的原因是

但是第一种方案:如果用户在登录后长时间不访问任何需要拦截的路径,那么他们的登录令牌可能不会得到及时刷新,导致令牌过期。一旦用户尝试访问需要拦截的路径,他们可能会发现自己需要重新登录,因为令牌已经失效。既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能

redis缓存相较于传统session:快速读写能力,处理大规模的并发请求,redis提供了持久性机制,丰富的数据结构:键值对存储数据库,分布式缓存

redis数据结构:String(缓存对象,分布式锁,共享session信息) ,List(对链表两端进行pop和opush操作,消息队列,设置唯一id),Set(聚合计算,交并比,点赞,共同关注,抽奖),Hash(缓存对象,购物车),ZSet(排序),stream(消息队列),GEO,bitmap进行签到,

选择合适的数据结构,比较hash和String的区别,设置合适的Key,选择合适的存储粒度,一般都是设置TTL为3min,

商户查询缓存

缓存:数据交换的缓冲区,降低用户访问并发量带来的服务器压力

缓存的使用降低了后端的负载,提高了读写效率,降低了响应时间

缓存更新:超时剔除,内存淘汰,主动更新

加入更新缓存,无效的读写操作较多,所以选择删除缓存策略,但是需要先操作数据库再删除缓存

双写方案:缓存调用者在更新数据库后再去更新缓存

由系统本身完成,数据库与缓存的问题交由系统本身去处理

调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

先操作数据库再去删除缓存

如何实现商铺和缓存与数据库双写一致?

对于查询商铺的代码,只需要加上缓存过期时间就可以实现双写一致,这里需要加上事务管理,因为更新数据库又删除了缓存

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

解决方案:缓存空值,布隆过滤

布隆过滤思想:(可能存在哈希冲突)

采用哈希思想,通过庞大的二进制数组,判断当前这个要查询的数据是否存在,如果布隆过滤器判断存在,放行,这个请求就访问到redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,

不存在,就直接返回空;

同时还需要预防缓存穿透,增加id复杂度避免被猜测id规律,做好基础格式校验,增江用户权限

缓存雪崩

同一时间段大量的缓存Key失效或者服务器宕机,导致大量请求到达数据库,带来巨大压力

给不同的key的TTL添加随机值

利用redis集群提高服务可用性

给缓存业务添加微服务

给业务添加多级缓存

缓存击穿(热点Key问题)

被高并发访问且缓存重建因为较复杂的Key突然失效了,无数的请求访问

给数据库带来巨大的压力

出现缓存击穿的问题,是因为我们对key设置过期时间,就不会出现缓存击穿的问题,但是不设置过期时间,数据就会一致占用内存

利用互斥锁

锁能够实现互斥性,用tryLock+double check的方法,但会有死锁的问题产生

利用redis的setnx方法获取锁,这里没有key则是查询成功

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);
}

利用jmeter进行压力测试,5秒发送2000请求,QPS为330

利用逻辑过期

逻辑过期一定需要进行缓存预热

  • 缓存中的数据不会被物理删除,而是设置了一个expireTime,标记数据的逻辑过期时间,当数据过期后,依旧返回旧数据,将过期时间设在redis的value中,查询时 ,先查询缓存是否命中,命中的话,就去查询缓存过期时间,如果已经过期,需要从数据库根据id查询数据更新缓存,但是会造成读取的都是脏数据

缓存重建包括查询数据库,写入缓存,耗时较长,同步执行,会阻塞当前线程,所以需要设置线程池异步执行

主线程判断缓存是否过期,过期的话,获取互斥锁并提交缓存重建任务到线程池,线程池中的线程,执行缓存重建任务,释放互斥锁,这里异步执行保证了主线程立即返回旧数据,同时提交缓存重建任务到线程池

这里我对数据预热进行乐Jmeter测试,设置逻辑过期时间为

最后进行封装Redis工具类

同步与异步的区别:

  1. 同步:任务按照顺序执行,前一个任务顺利完成后,才能执行下一个任务
  2. 异步:任务可以并发执行,前一个任务未完成时,后续任务也可以开始执行。需要对线程进行管理,任务调度

当进行异步处理,限制并发线程的数量,任务调度(定期执行任务或者延迟执行任务)需要用到线程池

@Slf4j
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;//线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public CacheClient(StringRedisTemplate stringRedisTemplate){this.stringRedisTemplate = stringRedisTemplate;}public void set(String key, Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);}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)));//写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));}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) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);return r;}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;}public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判断命中的是否是空值if (shopJson != null) {// 返回一个错误信息return null;}// 4.实现缓存重建// 4.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败,休眠并重试Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.获取锁成功,根据id查询数据库r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.释放锁unlock(lockKey);}// 8.返回return r;}private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key) {stringRedisTemplate.delete(key);}}

优惠券秒杀

这是高并发,高性能的场景,需要保证数据一致性和处理高并发请求,用异步处理+redis+lua脚本处理这类问题
异步处理:大量用户同时请求下单,每个请求都操作数据库,数据库响应慢,通过异步处理将下单请求和实际订单创建解耦
用户发送请求后,系统立即返回响应,将请求放入消息队列中,削峰削谷,使用RabbitMQ将下单请求异步化
使用Lua脚本库存扣减和用户资格判断,保持原子性
最后将数据持久化到数据库中
用set对存储在redis中的库存和订单进行存储
为了避免所有操作都在数据库上执行,分离两个线程,一个判断用户的购买资格,发现用户有资格后开启独立线程处理耗时较久的减库存和写订单操作
将较短耗时的操作放在redis中

Intern将字符串添加到字符串常量池中,并返回常量池中的引用。确保相同的字符串在内存中只有一份

        synchronized (userId.toString().intern()){//获取代理对象(事务)IVoucherOrderService prox = (IVoucherOrderService) AopContext.currentProxy();return prox.createVoucherOrder(voucherId);}

synchronized (userId.toString().intern()):

  • intern():确保相同的 userId 字符串在内存中只有一份,避免多个线程使用不同的字符串对象作为锁,导致锁失效。

使用代理(不修改目标对象代码的情况下,增强其功能)对象:spring的事务是基于AOP实现的,只有在通过代理调用方法时,事务才会生效,如果直接调用 createVoucherOrder(voucherId),相当于绕过了代理对象,事务注解(如 @Transactional)不会生效。

代理的作用

事务管理,spring利用代理对象拦截防范调用,并在方法执行前后进行事务管理;还可以增强AOP,应用切面逻辑,延迟加载,只有真正需要时才加载数据

解决库存超卖的现象:

  • 利用乐观锁代表为CAS:其中乐观锁只有在提交事务时才会检查数据是否被其他事务修改过,通常用版本号或者时间戳检测,如果修改过,就放弃当前事务
  • CAS则是实现原子操作的低级指令,包括内存地址,预期值,新值,CAS指令会检查内存地址V处的值是否等于预期值A,如果相等,则将该位置的值更新为新值B;其操作是原子的
  • 悲观锁:认为线程问题一定会发生,操作数据之前会获取锁,确保线程串行执行,synchronized Lock都属于悲观锁

通过加锁实现一人一单:d按无法解决集群下的一人一单问题

  1. 单机模式和集群模式:

单机模式指单个节点,用户访问呢和请求都是通过一台主机进行

集群模式:通过负载均衡的组件将>=2的主机搭建成一个集群方式,通过轮训和权重进行分配具体的机器,保证了服务高可用,不中断服务,可以通过心跳机制监听服务是否可用

负载均衡

将请求分发到多个服务器或资源的机制,目的是优化资源使用、最大化吞吐量、最小化响应时间,并避免单点故障。

redis主从、哨兵、集群各自架构的优点和缺点对比

  1. 主从复制:将一台redis服务器的数据复制到其他redis服务器中,但是存在数据冗余问题

  1. 哨兵模式:接着主从复制模式下,当更改主节点时,主节点的IP已经改变,但是假如应用服务依旧访问旧的IP,哨兵在这里就实现了自动化的故障恢复

访问redis集群的数据都是经过哨兵集群,哨兵控制整个redis集群,当主节点挂掉后,更换主节点,哨兵与应用服务进行交互

当然,不只是解决这些问题,redis的sen每个Sentinel以 每秒钟 一次的频率,向它所有主服务器从服务器 以及其他Sentinel实例 发送一个PING 命令。

如果一个 实例(instance)距离最后一次有效回复 PING命令的时间超过 down-after-milliseconds 所指定的值,那么这个实例会被 Sentinel标记为 主观下线

如果一个 主服务器 被标记为 主观下线,那么正在 监视 这个 主服务器 的所有 Sentinel 节点,要以 每秒一次 的频率确认 该主服务器是否的确进入了 主观下线 状态。

如果一个 主服务器 被标记为 主观下线,并且有 足够数量 的 Sentinel(至少要达到配置文件指定的数量)在指定的 时间范围 内同意这一判断,那么这个该主服务器被标记为 客观下线

在一般情况下, 每个 Sentinel 会以每 10秒一次的频率,向它已知的所有 主服务器 和 从服务器 发送 INFO 命令。

当一个 主服务器 被 Sentinel标记为 客观下线 时,Sentinel 向 下线主服务器 的所有 从服务器 发送 INFO 命令的频率,会从10秒一次改为 每秒一次。

Sentinel和其他 Sentinel 协商 主节点 的状态,如果 主节点处于 SDOWN`状态,则投票自动选出新的主节点。将剩余的 从节点 指向 新的主节点 进行 数据复制

当没有足够数量的 Sentinel 同意 主服务器 下线时, 主服务器 的 客观下线状态 就会被移除。当 主服务器 重新向 Sentinel的PING命令返回 有效回复 时,主服务器 的 主观下线状态 就会被移除。tinel最小配置:一主一从

  1. 集群模式:高可用,可拓展性,分布式,容错

通过数据分片进行数据共享,提供数据复制和故障转移把数据进行分片存储,当一个分片数据达到上限的时候,就分成多个分片。

集群键空间被分为16384(2^14)个hash槽,通过hash方式将数据分到不同分片上,读请求分给slave节点,写请求被分给master节点,实现了读写分离

全局id生成器

防止用户猜测出敏感信息,另一方面,当数据过大后,需要进行分表,分表之后不能出现相同id

符号位+序列号+时间戳

使用UUID,redis自增 ,snowflake算法,数据库自增

秒杀下单

用jmeter进行测试:出现超卖现象,95.80%

悲观锁:synchronized,Lock

乐观锁:版本法,CAS法(原子性操作)ABA,自旋CPU,并发性问题

判断秒杀时间开始或者结束没?库存是否充足

使用jmeter进行测试库存是否超卖,导入jmx文件

在这里我们要修改HTTP请求下面的登陆状态头。先去获取token。如何获取呢,选择我的,之后点击NetWork下的me请求,发现Request Headers下面存在Authorization ,将Authorization的值复制到HTTP请求头中。

超卖问题-多线程问题-加锁

使用乐观锁,当100个用户去查询它的值时,只有一个人扣减成功,另外的人查询到的都是修改过的库存,所以扣减失败,适合更新数据

使用悲观锁,适合插入数据

单体情况下的超卖和一人一单问题?乐观锁和悲观锁

分布式锁:满足分布式系统或者集群模式下多进程可见且互斥的锁

多线程可见,互斥,高可用:持有锁的节点发生故障,系统 

集群环境下:

先利用添加过期时间,解决死锁问题,但是过期时间可能会导致误删锁,但是给锁添加标示后,又会因为过期时间导致锁被提前释放,导致错误释放了获取到锁的下一个线程的锁,引发了原子性问题,所以我们通过Lua脚本解决此问题

分布式让大家使用用一把锁,就能锁住线程,让程序串行执行

满足:可见性,互斥,高可用,高性能,安全性,满足分布式系统下或者集群模式下多线程互斥且可见的锁

常见的分布式锁:

实现分布式锁

首先,增加线程标示:

解决redis分布式锁误删的问题

在获取锁之前存入线程标识(UUID),在释放锁的时候获取锁中的线程标示,判断是否与当前标示一样

private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";
@Override
public boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = ID_PREFIX +  Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}@Override
public void unlock() {//获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁中的标识String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX+name);//判断标识是否一致if(threadId.equals(id)) {//通过del删除锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}

解决分布式锁的原子性问题:

当线程1执行后,锁到期了,redis自动释放锁。线程2获取了这个锁,但是线程1并不知道锁已经被释放了,所以会继续执行删除锁的操作,就会导致线程2的锁被误删

可以看出线程1在删除锁之前,检查锁是否是自己的,但是检查锁和删除锁是两个独立的操作,之间可能存在时间间隔

if (redis.get(lockKey).equals(threadId)) { // 检查锁是否属于自己redis.del(lockKey); // 删除锁
}

getdel 之间,锁可能已经到期并被其他线程获取。

使用lua脚本解决原子性问题:

lua在redis中是原子执行的,所以可以确保检查和删除锁的操作是原子的。

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

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}public void unlock() {// 调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());
}
经过以上代码改造后,我们就能够实现 拿锁比锁删锁的原子性动作了~

redisson分布式锁

使用setnx锁存在以下问题:

不可重入,不可重试,超时释放,主从一致性

所以提出了redisson,redis实现的java内驻数据网络,提供了分布式锁和同步器

需要配置config

@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379") //redis://192.168.150.101:6379.setPassword("123456");// 创建RedissonClient对象return Redisson.create(config);}

可重入锁:包含三个参数:利用hash结构记录下线程id和重入次数

可重试:利用信号量和PubSUb实现等待,唤醒,获取锁失败的重试机制

超时续约:利用watchDog,每隔一段时间,重置超时时间

秒杀优化

利用lua脚本实现秒杀库存,一人一单,界定用户是否抢购成功

判断库存是否充足,用户是否重复下单,扣减库存,保存用户


-- 1. 参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1]
-- 1.2 用户id
local userId = ARGV[2]
-- 1.3 订单id
local orderId = ARGV[3]
-- 2. 数据key
--2.1 库存key
local stockKey = 'seckill:stock:' .. voucherId
--2.2 订单key
local orderKey = 'seckill:order:' .. voucherId
--3.脚本业务
--3.1 判断库存是否充足 get stockKey
if(tonumber(redis.call('get',stockKey)) <= 0) then--3.2 库存不足 返回1return 1
end
--3.2 判断用户是否下单SISMEMBER orderKey userId
if(redis.call('sismember', orderKey,userId)==1) then--3.3 存在,说明是重复下单,返回2return 2
end
--3.4 扣库存 incrby stockKey -1
redis.call('incrby', stockKey,-1)
--3.5 下单(保存用户)sadd orderKey userId
redis.call('sadd',orderKey,userId)
--3.6 发送消息到队列中 XADD stream.orders * k1 v1 k2 v2
--redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

利用redis完成库存余量查询,一人一单,完成抢单业务,再将下单业务放在阻塞队列中,利用独立线程异步下单,但是会出现内存限制和数据安全问题

接下来是消息队列异步优化秒杀接口,

消息队列

存储和管理消息,也成为消息代理

redis消息队列:

list消息队列,双向链表

基于pubSub的消息队列:

消费者可以订阅一个或者多个channel,生产者像对应的channel,所有的订阅者都可以接收到消息,支持多生产,多消费

基于stream的消息队列——消费者组:

将多个消费者划分到一个组中,监听同一个队列

我们利用redis的stream结构作为消息队列,实现异步秒杀下单

创建名为stream.orders的消息队列,当认定其有抢购资格后,像其中添加消息

简单的业务实现

利用利用hashSet判断是否点赞,SortedSet实现点赞排行榜,利用Set实现共同关注,利用推模式实现关注推送功能,收件箱需要进行分页查询和角标变化的注意,使用位运算进行bitMAp计算连续签到天数

达人探店:

Feed流:

TimeLine:不做内容筛选,简单按照发布时间排序,推模式,拉模式,推拉模式结合

智能排序:利用智能算法屏蔽掉违规的,用户不感兴趣的内容

利用推模式,将用户的笔记推送到粉丝的邮件箱,收件箱根据时间戳进行排序,利用redis的数据结构进行实现

滚动分页查询参数:

max:上一次查询的最小值

Min:0

offset:上一次查询时,与最小值的一样的元素个数

count:3

返回到页面的具体实现类:ScrollResult;

  • 导入GEO数据结构到店铺
  • 用户签到bitMap

项目逻辑条理分析

短信登录:首先通过session进行登录,再过渡到redis的登录,使用ThreadLocal作为存放用户信息的载体

首先进行短信验证码的发送

短信验证码:验证手机号格式-》生成验证码-》发送验证码

短信验证码登录注册:提交手机号和验证码,一致,根据手机号查询用户信息,无则新建,将用户信息保存到session中

校验登录状态:请求并携带cookie,从session中获取用户,判断用户是否存在,存在,保存用户到ThreadLocal中

由于tomcat不共享用户的session存储,如果每个节点实现session的拷贝的话,浪费内存,所以提供了redis缓存存放用户数据的信息

细节处理:

1.这里将用户部分的属性进行了封装成单独的DTO中,并且使用随机token作为key进行存储

2.刷新token,设置了双登录拦截器,第一次拦截所有的页面,第二次只需要对user是否存在进行判断就可以了,可以刷新token有效期,保证了用户不会因为时间过期而重新登录

商户缓存功能:

查询商户信息时,通常使用缓存提升访问速度,缓存就是进行数据交换的地方,通常先查询缓存,再查询数据库、

根据id查询数据库:

  • 但是同时会出现双写一致的问题:

redis的缓存更新包括:内存淘汰,超时剔除,主动更新

我们采用主动更新的策略,更新数据库的同时更新缓存,更新数据库再删除缓存:

所以根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间

根据id修改店铺时,先修改数据库,再删除缓存。等下一次再来查询数据时,再从数据库中加载到缓存中。

  • 缓存穿透问题:

client->缓存->数据库,这些请求都达到了数据库,造成数据库的压力过载

缓存空值和布隆过滤器

这里使用缓存空值的方法:

先从前台提交要查询的商户id,打到redis中判断是否命中缓存,如果redis中命中缓存了,再判断缓存是否是空值,如果是的话,直接结束查询,不是的话就返回数据。如果redis没有命中的话,就来到数据库中查询,在数据库中查询到了数据,便将数据写入缓存,同时返回数据。如果没有的话,直接将空数据写入缓存,结束查询。

  • 缓存雪崩:同一时间段大量的Key失效或者是redis宕机,导致大量请求到达数据库,带来巨大压力。

解决方法:

  • 缓存穿透问题

被高并发访问的且缓存重建的key失效无数请求访问到数据库中

之所以会出现这个缓存击穿(热点key)问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。

用互斥锁和逻辑过期解决:

互斥锁:redis中setnx进行

逻辑过期:用户根据id查询商铺,假如查询redis没有命中,就看是否过期,过期了就去获取互斥锁,获取互斥锁成功后,开启独立的线程去查询数据库,异步的缓存构建,但是会读取到脏数据

优惠券秒杀:

  • 全局唯一id,优惠券秒杀解决单体架构的一人一单,乐观锁,悲观锁的使用
  • 超卖问题:多线程安全问题,当相同的线程去查询库存发现,stock>0,同时进行库存-1,为了解决这个问题,提出了乐观锁和悲观锁两个概念,这里我用了乐观锁去匹配库存是否改变,没有改变,就更改库存-1,乐观锁适合更新数据,悲观锁适合插入数据
  • 解决一人多单问题:根据id查询优惠券id,查询到了优惠券信息后,判断秒杀是否开始,如果开始的话,判断库存是否>0,是的话就根据id查询订单,订单存在返回异常,不存在扣减库存,创建订单

秒杀优化:

一人一单在集群的环境下,就出现了异常,所以我们引入了redis的分布式锁,利用redis的setnx满足分布式或者集群下的多线程可见且互斥的锁,利用setnx加锁,同时加上过期时间

但是还会有误删锁的情况,所以增添了线程标示,同时线程的拿锁,比锁并不是原子性的,者之间也可能会因为误删,所以增加lelua脚本实现加setnx的分布式锁

但是setnx锁依旧存在着许多的问题,包括不可重入,不可重试,超时释放,主从一致性,所以引入了redisson的概念

WatchDog 机制是 Redisson 提供的一种自动延期机制。当线程获取锁成功后,Redisson 会启动一个 WatchDog 线程,每隔一段时间(默认是 30 秒)自动延长锁的超时时间,确保锁不会因为超时而被释放。这可以避免因业务逻辑执行时间过长导致的锁过期问题,提高分布式锁的稳定性和可靠性。

秒杀优化的部分:引入异步,消息队列

  • 新增秒杀优惠券的同时,将优惠券信息保存到Redis中
  • 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
  • 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
  • 基于Redis的Stream结构作为消息队列,实现异步秒杀下单。开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

好友关注功能:feed流投喂方式,和滚动分屏的实现

附近商户查询实现:以当前坐标作为圆心,同时绑定相同的店家类型type,以及分页信息,把这几个条件传入后台,后台查询出对应的数据再返回。

用户签到功能:利用redi中的bitmap数据结构存储签到信息,把每一个bit位对应当月的每一天,形成了映射关系。

http://www.xdnf.cn/news/5509.html

相关文章:

  • AI赋能安全生产,推进数智化转型的智慧油站开源了。
  • BUUCTF——PYWebsite
  • 记一种C#winform小程序的简易打包方式-自解压压缩文件
  • 火山RTC 7 获得远端裸数据
  • MATLAB机器人系统工具箱中的loadrobot和importrobot
  • Voice Changer 变声器
  • C++语法基础(上)
  • linux内核pinctrl/gpio子系统驱动笔记
  • 并行发起http请求
  • Spring Cloud : OpenFeign(远程调用)
  • 腾答知识竞赛系统 V1.0.4更新
  • Linux文件编程——open函数
  • CAPL -实现SPRMIB功能验证
  • 《操作系统真象还原》第十四章(1)——文件系统概念、创建文件系统
  • 写屏障和读屏障的区别是什么?
  • 思维链是仅仅通过提示词实现的吗
  • Java对象的内存分布(二)
  • Python训练营打卡——DAY22(2025.5.11)
  • UGMathBench动态基准测试数据集发布 可评估语言模型数学推理能力
  • Maven 中的 pom.xml 文件
  • Mind Over Machines 公司:技术咨询与创新的卓越实践
  • redis存储结构
  • UOJ 164【清华集训2015】V Solution
  • 【C语言】程序的预处理,#define详解
  • 用于文件上传的MultipartFile接口
  • Go语言实现优雅关机和重启的示例
  • 自然语言处理 (NLP) 入门:NLTK 与 SpaCy 的初体验
  • 『 测试 』测试基础
  • nanodet配置文件分析
  • 快速理解动态代理