科院美食~

目录

登入功能:

发送验证码:

qq邮箱:

短信验证码:

发送验证码:

登入验证:

拦截器:

缓存刷新拦截器:拦截所有

登入拦截器:除白名单以外的所有

axjos拦截器:

登入检查:

商铺:

查询商铺:

修改商铺:

优化查询商铺:

缓存穿透问题:

缓存雪崩问题:

缓存击穿问题(热点key问题):

优惠卷秒杀:

全局唯一ID生成策略:大概可以用69年

Redis自增:

雪花算法:

生成全局ID流程:

生成订单流程:

超卖问题(多线程安全问题):典型解决方案就是加锁

悲观锁:

乐观锁:

一人一单问题:

出现并发问题:一个人会抢很多单

新问题:service层加了事务后,高并发下,还是无法实现一人一单

新问题:事务会失效

新问题:集群环境下(多个Tomcat服务器启动了)的并发问题

新问题:分布式锁误删问题

新问题:释放锁时在判断是不是自己锁的时候,判断过后再次误删

优化:提高效率

问题:JVM内存大小限制,和数据安全等问题

达人探店:

发布探店笔记:

查看探店笔记:

点赞功能:set实现

点赞排行榜功能:SortedSet实现(分数用当前的时间戳)

点赞:

点赞排行榜:

好友关注:

推送功能:

传统分页:

滚动分页:

修改-发布探店笔记:

推送的实现:

附件商户:

前奏:

原因:

实现:

用户签到:利用Redis中的BitMap数据结构

签到功能:

统计连续签到:


登入功能:

发送验证码:

qq邮箱:

第一步添加依赖:

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
     </dependency>

第二步:配置配置文件

spring:
  mail:
    host: smtp.qq.com	//用哪个邮箱
    username: 2640806248@qq.com	//用户名
    password: meuzbwvapbthdjje	//授权码
    default-encoding: utf-8		//编码方式
    properties:				//开启加密规则
      mail:
        smtp:
          ssl:
            enable:true

第三步:service文件编写:

1-配置    JavaMailSender类,用@Autowired注入,发送邮件用的
2-配置用户名,用@Value("${spring.mail.username}"),读取配置文件中的用户名
3-new一个mailMessage类,邮件消息类
进行对应设置
        mailMessage.setSubject("验证码邮件");    //标题
        mailMessage.setText("您收到的验证码是:"+code); //内容
        mailMessage.setTo(phone);    //发到哪去
        mailMessage.setFrom(from);    //谁发
        mailSender.send(mailMessage);    //发送

短信验证码:

用的腾讯云生成的代码,调试一下就可以用了

发送验证码:

发送验证码:
参数(手机号码)
1-检查手机格式是否正确:
2-获取一个4位数的随机验证码
3-把验证码存入redis中(key为手机号码)
4-把验证码发送到对应手机号码(或者邮箱中)

登入验证:

拦截器:

缓存刷新拦截器:拦截所有

1-拿到token
2-根据token拼接key到Redis中查用户
3-有用户,把用户存到UserHodle中,方便后续使用
4-刷新token有效期
5-放行

登入拦截器:除白名单以外的所有

1-从UserHodle中取用户
2-取到用户就放行

前端axjos根据返回值:如果页面没有被拦截,返回ok就正常访问;反之如果被拦截或返回fail,就跳转到登录页面

axjos拦截器:

每次发送请求之前进行拦截,把我们自己加的token加入到请求头中,再进行发送

登入检查:

参数(手机号码,验证码1)
1-检查手机号码格式是否正确
2-利用手机号码在Redis中找到验证码
3-利用2个验证码进行校验
4-查找数据库看是否有这个人
5-没有就进行创建
5-把这个人存入Redis中(key为uuid随机创建的)
6-返回token(key)

=========================================================================

商铺:

查询商铺:

对象存入Redis:对象某个字段频繁修改用Hash,一般还是用Stirng吧!
Set类型:无序,元素不可重复,支持交集,并集,差集
List类型:有序,元素可重复,插入删除快,查询速度一般(只能用角标查询)
SortedSet类型:可排序,不可重复,查询速度快(能用角标,也可以用sorce值范围)
添加商铺缓存:
1-根据id去Redis中查找店铺信息
2-查到,转为对象进行返回
3-没查到,去数据库查找
4-把数据库查到的对象转换为json字符串保存到redis中
5-返回数据库查到的对象

修改商铺:

问题:
删除缓存还是更新缓存:删除,更新的无效写操作太多了
如何保证操作的同时性:分在一个事务中
先操作数据还是先删除缓存:先操作数据库,因为删除缓存比操作数据库快,2人中间的间隙时间(后一步的操作时间)就少了

解决方案:
先修改数据库再删除缓存(保证缓存与数据库的双写一致)
 

优化查询商铺:

缓存穿透问题:

访问的数据根本不纯在,这些请求都会打在数据库上。

解决办法:

缓存空对象进行解决,在数据库中没查到,不直接返回,照样存入redis,只不过值为“”;(stringRedisTemplate查不到值时返回值为null)

缓存雪崩问题:

大量的缓存key同时失效或者Redis宕机,导致大量请求直接打到数据库上
解决办法:
1-给不同的key的TTL设置随机值,避免大量缓存key同时失效。
2-利用Redis集群提高服务的可用性

缓存击穿问题(热点key问题):

一个被高并发访问并且缓存重建业务较复杂的key突然失效了,导致大量请求打到数据库上
解决办法:
1-互斥锁方案:
    1-根据id去redis中查找商铺信息
    2-查到,转为对象进行返回
    3-没查到,看看是否为“”
    4-为“”,返回数据库不存在错误信息
    5-不为“”,获取互斥锁
    6-获取失败,睡眠一段时间,继续获取
    7-获取成功,去数据库查数据
    8-没查到,存入“”对象,返回错误信息
    9-把数据库的数据存入redis中
    10-释放互斥锁
    11-返回数据库查到的对象


2-逻辑过期方案:
    1-根据id去redis中查找商铺信息
    2-没查到,返回错误,无此该商铺
    3-查到,判断是否逻辑过期
    4-没过期,返回该数据
    5-过期,获取(互斥锁)开启独立线程
    6-返回旧的数据
    
    独立线程:
    1-根据id查询数据库
    2-把数据库查到的数据设置到Redis中,设置逻辑过期时间
    3-释放互斥锁
=========================================================================

优惠卷秒杀:

全局唯一ID生成策略:大概可以用69年

Redis自增:

每天一个key,方便统计订单量
ID构造是 时间戳 + 计数器(31位时间戳,32位计数器)--秒
技巧:利用Redis集群,5个,让他们自增的值为5,然后初始值设置为0,1,2,3,4,即可保证还是不一样

雪花算法:

ID构造是 符号位+时间截(41)+机器的ID(10)+流水号(12)(计数器)--毫秒
特点:不依赖第三方系统,依赖机器时钟。

生成全局ID流程:

1-确定开始时间
2-确定计算器位数
3-获取时间戳
4-获取当前日期,格式为 yyyy:MM:dd
5-开始自增长
6-拼接并返回

生成订单流程:

参数:优惠卷ID
1-去数据库中获取优惠卷信息
2-获取失败,返回错误信息,该优惠卷已失效
3-获取成功,获取开始时间,判断是否开始
4-获取结束时间,判断是否结束
5-获取库存信息,判断是否库存不足
6-扣减库存
7-生成订单信息
8-返回订单编号(便于之后跳转到对应的支付页面)

超卖问题(多线程安全问题):典型解决方案就是加锁

悲观锁:

Synchronized,Lock(认为线程安全问题一定会发生)

乐观锁:

在 更新 数据时判断,判断是否有其他线程对数据做了修改,被修改则重试或者停止,插入无法使用乐观锁,因为数据库中都没有数据,你无法判断是否改变过。
      版本号法:每一次更改数据之前会检查版本号有没有发生改变,每一次修改数据之后会给版本号+1;
      CAS法:通过将内存中的值和数据库中指定数据进行比较,当数据一致时(或满足默写条件时),才会进行更改操作(会出现ABA问题)

解决办法(解决ABA问题):
1-版本号法:加版本控制
2-CAS:更新的sql语句中加一个wher stock = voucher.getStock()-->wher stock>0

一人一单问题:

在库存减一之前,加一个查询语句(查订单列表里面有没有这个用户对这个优惠卷的购买),查询不为空,提前结束。

出现并发问题:一个人会抢很多单

解决办法:添加悲观锁:synchronized(同步监视器)
       1-方法上加:
            非静态方法:this-    Runnable
            静态方法:类本身 -    Thread
          2-语句上加
            自己指定同步监视器。
法一:封装方法
查询订单-扣减库存-生成订单信息-返回订单id
方法上+synchronized

优化方案:因为这样就出现不同用户也会串行执行,按道理不同用户应该可以并行执行的。
方案:在语句+synchronized,同步监听器:userId.toString().intern()
intern() 这个方法是从常量池中拿到数据,如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象,new出来对象,我们使用锁必须保证锁必须是同一把,所以我们需要使用intern()方法

新问题:service层加了事务后,高并发下,还是无法实现一人一单

众所周知,我们在spring中使用@Transactional事务注解,那么这个事务的开启和提交是spring利用aop帮我们自动完成的。说白了就是执行方法之前spring帮我们开启事务,方法执行完毕后spring再帮我们把事务提交。而方法的执行和事务的开启及提交并不是一个原子操作。所以方法执行完毕以后事务并没有提交。所以我们无论是将synchronized关键字加再方法体上还是用代码块的方式,只能保证方法中的代码或synchronized代码块中的代码执行的原子性。所以在高并发的情况下,就极有可能出现一些线程service方法已经执行完毕或synchronized代码块已经执行完,但是事务还没有提交(数据库中的值并没有改变),另一些线程就开始读取数据库中的值(没有提交事务之前的值)那么就有可能多个线程读取的是同一个值。所以就会出现和我们预想的结果不一致的情况。所以在并发程序中,而且还牵扯到事务的情况下,要特别注意这一点。

解决办法:
1-把要求添加事务的代码抽成一个方法,然后把这个方法添加到synchronized里面
2-自己控制事务,把事务写到synchronized里面

新问题:事务会失效

因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用service类的代理来生效

解决办法:

创建代理对象:
1-添加依赖aspectjweaver

    <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
    </dependency>


2-启动类添加注解:@EnableAspectJAutoProxy(exposeProxy=true)

@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {

    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }

}


3-AopContext.currentPoxy()方法创建代理对象。

IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);//代理对象,调用事务方法

新问题:集群环境下(多个Tomcat服务器启动了)的并发问题

原因是锁对象在不同的jvm中他不一样(每个Tomcat都有一个属于自己的JVM,每一个JVM都有自己的锁。)

解决办法:用Redis分布式锁,就是在调用一人一单方法(需要保持串行执行的代码)之前,得先获取Redis锁,方法(放在try中)运行结束后,释放Redis锁(放在finally中)

新问题:分布式锁误删问题

持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删

解决办法:在删除锁之前,取出锁内的值,看看是不是自己的锁,是自己的再进行删除。但是,ThreadId其实是JVM维护的一个递增的计算器,不同JVM是有可能相同的,所以应该在设置锁和获取锁的时候添加一个前缀(UUID可以胜任),保证每次获取一个锁对象的时候他的UUID都是不相同的,故而保证了每一个UUID+线程id组成的key都是不相同的。

新问题:释放锁时在判断是不是自己锁的时候,判断过后再次误删

释放锁时在判断是不是自己锁的时候,判断成功,但是突然阻塞,导致锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,再次误删

解决办法:lua脚本类似于sql语句,可以保证原子性。,把判断和释放锁的Redis代码都放到lua脚本里面,然后直接用stringRedisTemplate的execute()方法执行就好了,

优化:提高效率

所有步骤串行执行(业务耗时等于每一步耗时之和),且要多次访问数据库(并发能力弱),所有业务系能很差(耗时长)。

解决方案:
把对资格的判断和写操作分离开,让他们异步进行。把耗时最长的写操作分出来了
对资格判断用redis,写入lua中。把对sql的访问变为了redis的访问,减少了耗时
如果判断完立即返回结果,如果成功还要把任务加入阻塞队列


解决的问题(lua脚本具有原子性):减库存(超卖问题),创建订单(一人一单问题)

lua:
1-判断库存是否大于0,如果小于零,返回1
2-判断set中是否有这个用户id,如果有,返回2
3-库存减一,把用户添加到redis中,返回0

主线程:
1-执行lua脚本
2-判断是否等于1,是就返回错误信息(库存不足)
3-判断是否等于2,是就返回错误信息(不可重复下单)
4-把订单对象封装后加入阻塞队列
5-返回用户id

分线程:
循环(添加事务)
1-取出阻塞队列中的任务(没有就一直阻塞等待)
2-减库存
3-创建订单

问题:JVM内存大小限制,和数据安全等问题

JVM内存大小有限,不限制可能会造成内存溢出等问题
数据安全问题(数据不一致问题):服务器宕机,从队列取出后出现异常

解决办法:用基于Stream的消费组组消息队列模式
已消费:pending-list(待确认集合)中
已确认:在pending-list中移除

lua:
1-判断库存是否大于0,如果小于零,返回1
2-判断set中是否有这个用户id,如果有,返回2
3-库存减一,把用户添加到redis中
4-把userId,voucherId,oredrId,加入消息队列,返回0
redis.call('XADD','stream.orders','*','userId',userId,'voucherId',voucherId,'id',orderId)

主线程:
1-执行lua脚本
2-判断是否等于1,是就返回错误信息(库存不足)
3-判断是否等于2,是就返回错误信息(不可重复下单)
4-返回用户id

分线程:

private class VoucherOrderHandler implements Runnable {

    @Override
    public void run() {
        while (true) {
            try {
                // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                    Consumer.from("g1", "c1"),
                    StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                    StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
                );
                // 2.判断订单信息是否为空
                if (list == null || list.isEmpty()) {
                    // 如果为null,说明没有消息,继续下一次循环
                    continue;
                }
                // 解析数据
                MapRecord<String, Object, Object> record = list.get(0);
                Map<Object, Object> value = record.getValue();
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                // 3.创建订单
                createVoucherOrder(voucherOrder);
                // 4.确认消息 XACK
                stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
            } catch (Exception e) {
                log.error("处理订单异常", e);
                //处理异常消息
                handlePendingList();
            }
        }
    }

    private void handlePendingList() {
        while (true) {
            try {
                // 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                    Consumer.from("g1", "c1"),
                    StreamReadOptions.empty().count(1),
                    StreamOffset.create("stream.orders", ReadOffset.from("0"))
                );
                // 2.判断订单信息是否为空
                if (list == null || list.isEmpty()) {
                    // 如果为null,说明没有异常消息,结束循环
                    break;
                }
                // 解析数据
                MapRecord<String, Object, Object> record = list.get(0);
                Map<Object, Object> value = record.getValue();
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                // 3.创建订单
                createVoucherOrder(voucherOrder);
                // 4.确认消息 XACK
                stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
            } catch (Exception e) {
                log.error("处理pendding订单异常", e);
                try{
                    Thread.sleep(20);
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}

=========================================================================

达人探店:

发布探店笔记:

1-上传照片
1-把userId封装到blog中
2-把blog保存到数据库中
3-返回blogId

查看探店笔记:

1-根据id查询blog
2-根据userId查user
3-把user的属性封装到笔记中
4-返回笔记

点赞功能:set实现

在Blog类中添加一个isLike字段,标示该用户是否被点赞(用于前端是否高亮)

1-每一个blog都在Redis中有一个set(key为blogId)集合,每次要修改点赞数目之前,都要去set中看看该用户是否存在
2-存在,点赞总数-1,set集合中把该用户id删去
3-不存在,点赞总数+1,set集合中把该用户id加上
4-修改根据id查询Blog的业务,根据Set集合判断是否点过赞,赋值给isLike字段
5-修改分页查询Blog业务,根据Set集合判断是否点过赞,赋值给isLike字段

点赞排行榜功能:SortedSet实现(分数用当前的时间戳)

点赞:

在Blog类中添加一个isLike字段,标示该用户是否被点赞(用于前端是否高亮)
1-每一个blog都在Redis中有一个SortedSet(key为blogId)集合,
  每次要修改点赞数目之前,都要去SortedSet中看看该用户是否存在(即查看分数是否为null)
2-存在,点赞总数-1,SortedSet集合中把该用户id删去
3-不存在,点赞总数+1,SortedSet集合中把该用户id加上
4-修改根据id查询blog业务,根据SortedSet集合判断是否点过赞,赋值给isLike字段
5-修改根据分页查询blog业务,根据SortedSet集合判断是否点过赞,赋值给isLike字段

点赞排行榜:

1-根据店铺id查找SortedSet中前五个用户
2-解析出其中的userId,String->Long(#.stream().map(Long::tovalue));拼接成逗号分隔的字符串StrUtil.join(",",#)
3-根据userId查找到user(查询sql语句末尾加一个"ORDER BY FIELD(id," + idStr + ")"),不加的话数据库内部排序会出问题。正确查询后转换为UserDTO对象
4-返回userDTOS

=========================================================================

好友关注:

判断是否关注了:
1-获取userId
2-根据set(key为userId)中是否有该用户id
3-返回true或者false

关注和取关:
1-获取登录用户id
2-判断是关注还是取关
3-关注,给数据库加一条数据,并在set(key为userId)集合中加入关注用户的id
4-取关,删除一条数据,在set集合中删除该用户
5-返回

共同关注:
1-获取登录用户Id
2-根据登录用户id和和当前用户id,得到他们Set的key,利用Set找到交集
3-解析交集(id集合)
4-根据id去数据库找到对应的user,然后转换为对应的userDTO
5-返回users

=========================================================================

推送功能:

传统分页:

按角标分页

滚动分页:

记录每一次查询的最后一个的值,下一次从最后有一个值的下一个开始
max:当前时间戳(一个极大值)| 上一次查询的最小时间戳(最大值)
min:0(最小值)
offset:0 | 在上一次结果中,和最小值一样的个数(偏移量)
count:3    (一页的消息条数 )

修改-发布探店笔记:

1-上传照片
1-把userId封装到blog中
2-把blog保存到数据库中
3-查询该用户(作者也就是登录用户)的粉丝列表
4-遍历粉丝列表,给每一个粉丝的SortedSet(key为粉丝ID)中加入该blogId(score :当前时间戳)
5-返回blogId

推送的实现:

定义返回值实体:List,minTime,offset

1-获取该用户
2-利用滚动分页查询,收查询信箱中的blogId集合,需要的参数max/offset都已经传过来了
3-非空判断
4-解析数据,遍历集合
4.1-获取value,把blogId转换为long加入ids集合中
4.2-获取分数,判断是否和最小值相同
4.3-如果相同,则newOffset+1;如果不同,则更新最小值,重置newOffset
5-根据ids去数据库查询blog(查询sql语句末尾加一个"ORDER BY FIELD(id," + idStr + ")")
6-遍历blos,给每一个blog添加用户信息和点赞信息
7-封装返回值实体,并返回
=========================================================================

附件商户:

前奏:

把所有店铺按照typeId进行分类,以typeId为key,以shopId为value,以经纬度为socer,以Geo形式存入Redis

原因:

Geo要经纬度和一个值,因为redis在内存中,所以不宜过大,最好存入的就是shopId;但是请求又要求以类型进行分类查询,如果全部Shop存入一个Geo中,就必须要用的typeId,但我们只传入了ShopId。所以最后的解决办法是,让不同的类型存入不同的Geo

实现:

1-判断是否是根据坐标进行查询
2-计算分页参数,from(从哪开始),end(到哪结束)
3-查询Redis,按照距离排序,分页
search():参数:key,圆心,半径,[搜索参数] 分页:只能指定结束位置,永远从0开始
解决办法,拿到之后进行截取,截取from到结束。(GeoLocation:值+点的坐标)
4-解析出ShopId,和距离
5-根据id查询Shop,并把距离封装到每一个shop中
6-返回shopList

=========================================================================

用户签到:利用Redis中的BitMap数据结构

签到功能:

1-获取当前登录用户信息
2-获取日期 
3-拼接key(用户Id:年:月)
4-获取今天是本月的第几天
5-写入redis (SETBIT key offset(天数-1,因为他是从0开始添加的) 1)

统计连续签到:

1-获取当前登录用户信息
2-获取日期 
3-拼接key(用户Id:年:月)
4-获取今天是本月的第几天
5-获取本月截止到今天为止的所有签到数据,返回的是一个十进制数组

6-循环遍历:
1-让这个数字和1做与运算,得到数字最后一个bit位
2-判断是否为0,为0表示未签到,结束
3-不为0,已签到,计数器加一,数字无符号右移一位(抛弃最后一位)num>>>=1

7-返回计数器的值

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值