分布式秒杀系统构建

分布式秒杀系统构建,第1张

目录
  • 主要解决问题
  • 项目架构
  • 数据库设计
  • 项目启动
  • 1.后端返回秒杀时间
  • 2.解决跨域问题
  • 3.Redis使用Cacheable
  • 4.秒杀倒计时
  • 5.如何防止提前下单
  • 6.如何防止重复提交
  • 7.网关限流
      • 计数器算法
      • 漏桶算法
      • *令牌桶算法
  • 8.请求下单数据一致性
  • 9.sql出错导致feign超时调用
  • 10.如何保证消息不丢失
  • sentinel防服务雪崩
      • 流量控制
      • 熔断降级

主要解决问题
  1. 高并发
  2. 线程安全(超卖)
  3. 事务一致性(分布式事务)
  4. 防止提前下单
  5. 倒计时实现
项目架构

数据库设计
CREATE DATABASE shop

-- 商品表
CREATE TABLE goods(
	id INT PRIMARY KEY AUTO_INCREMENT,
	title VARCHAR(100) NOT NULL,
	info TEXT,
	price DECIMAL(10,2),
	save INT NOT NULL,
	begin_time TIMESTAMP NOT NULL,
	end_time TIMESTAMP NOT NULL
);

-- 订单表
CREATE TABLE orders(
	id INT PRIMARY KEY AUTO_INCREMENT,
	oid VARCHAR(30) NOT NULL UNIQUE,
	gid INT NOT NULL,
	uid INT NOT NULL,
	gnumber TINYINT NOT NULL DEFAULT 1,
	all_price DECIMAL(10,2) NOT NULL,
	create_time TIMESTAMP DEFAULT NOW(),
	STATUS TINYINT DEFAULT 0
)
项目启动

Nacos
cmd D:
cd Program Files\nacos\bin
startup.cmd -m standalone
http://192.168.52.1:8848/nacos/index.html
启动Sentinel
cd Program Files
java -jar sentinel-dashboard-1.8.1.jar
localhost:8080账号密码sentinel
Reids
启动CentOS8
cd usr/local/redis/bin
./redis-server redis.conf
./redis-cli
auth 000
RabbitMQ
启动CentOS7
/usr/local/software/rabbitmq_software/rabbitmq_server-3.7.16/sbin/rabbitmq-server -detached
http://192.168.32.129:15672/
启动前后端各服务

1.后端返回秒杀时间

如果用前端js,时间就参考本机的时间,为了保证所有时间一致,使用服务器返回时间

基本所有时间有关的都由服务器返回

2.解决跨域问题

浏览器和gateway网关之间会产生跨域问题

因为浏览器的请求会先到网关

spring:
  cloud:
    gateway:
      # gateway的全局跨域请求配置
      globalcors:
        cors-configurations:
          '[/**]':
            allow-credentials: true
            allowed-originPatterns: "*"
            allowed-headers: "*"
            allowed-methods:
              - OPTIONS
              - GET
              - POST
3.Redis使用Cacheable

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-cacheartifactId>
            <version>2.6.6version>
        dependency>


@Cacheable:自动从Redis中找,没有从数据库查完放到Redis
@CachePut:一旦这个方法被调用,直接删除这个指定的key
太方便了

序列化问题:增加配置类

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        config=config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
        return config;
    }
}
4.秒杀倒计时
  1. 客户端进入商品页面,随即从服务器获取时间
  2. 获取该场次时间,相减获得时间差
  3. 使用setTimeout()让服务器时间每隔一秒加一秒
  4. 时间差小于等于0,则改变按钮,开始秒杀

有一个专门处理时间的服务器,这个服务器可以被其他服务调用,这样保证了在集群情况下,各个服务器中的时间误差达到微妙级别

5.如何防止提前下单

方案一:
下单前,查询缓存,获得当前商品的秒杀开始时间,看当前时间是否在秒杀时间之内,就可以下单;(redis中key过多,需要判断)

方案二:
通过Redis记录当前秒杀时间段的商品集合,这样Redis中key不会太多

  1. 管理员添加秒杀商品,存入数据库,再在Rides的该时间段的集合中加入该商品
  2. Redis开定时任务,时间到了更变当前秒杀商品集合
  3. 有下单请求,先在Redis的当前时间集合中查看是否有该商品Id,有就说明该商品正在秒杀时间段,完成购买;没有就说明不在,返回错误。
6.如何防止重复提交

方案一:
商品页面生成时,会在页面隐藏一个UUID,提交请求后把这个UUID存入Redis,如果这个UUID已存在,就是重复提交
(只能防止误触碰;如果有人恶意提交不能判断)

方案二:
提交必须输入验证码

  1. 防止重复提交
  2. 防止恶意提交(秒杀器、脚本)(普通验证码容易被破解;现在是行为验证码)
  3. 拉长服务器的请求处理时间
7.网关限流 计数器算法

每次请求计数器加1,达到一定阈值,停止接收请求,单位时间后清零,这样可以控制在单位时间内接收的请求数量。

如果单位请求分布不均匀,会超出阈值

漏桶算法
  1. 水(请求)从上方倒入水桶,从水桶下方流出(被处理);
  2. 来不及流出的水存在水桶中(缓冲),以固定速率流出;
  3. 水桶满后水溢出(丢弃)。

这个算法的核心是:缓存请求、匀速处理、多余的请求丢弃。

漏桶算法对突发流量不做额外处理,无法应对存在突发特性的流量

*令牌桶算法

令牌桶可以看成一个集合,有当前令牌数和最大令牌数两个参数

按照一定的速率往令牌桶中放令牌

请求还可以根据自身的量级来拿令牌数;

还可以根据其他因素限流。比如说普通用户要3个令牌,vip只要一个令牌;

桶放在Redis中,Key不同类,可以根据IP、URL限流

lua脚本


实现
路由网关先查询Redis中是否存在令牌,如果存在再取领牌(count–);这是两条命令,会发生安全性问题。Redis执行命令是单线程可以不用加锁,使用lua脚本或Redis事务

令牌桶的结构

hash结构,key为需要限流的关键属性(url),value为多个参数

  1. 当前剩余的令牌数
  2. 最大令牌数
  3. 每秒产生的令牌数
  4. 下一次可以产生令牌的时间

设计要点

  1. 令牌的生成方式:不用线程添加,而是每次请求主动计算要生成多少
  2. 令牌的预支设计:为了让重量级的请求有机会执行;预支的是时间,而不是令牌数;比如每秒生成60个令牌,如今我有30个令牌,这个请求要50个令牌,就会预支到0.5秒后的时间,而在这0.5秒之中如果有个请求在0.2需要预支令牌,告知它需要等待0.3秒;
8.请求下单数据一致性

单机

  1. 使用sync加锁,锁在业务层,但是事务靠aop加强,如果锁释放事务没提交却有线程修改,还是会出现问题,耗时15s
  2. 使用redis分布式锁,IO消耗性能,107s
  3. 使用mysql锁:开启数据库事务,使用select ... for update给查询的行加上排他锁,保证读和该原子性,耗时11s
  4. mysql乐观锁:尝试扣减库存,修改时这条数据会自动加锁,返回是否被修改,然后执行后面的订单业务,耗时6s
  5. redis的lua脚本:把库存放到redis,使用lua脚本在reids查询并下单,最后把redis数据同步到数据库,无锁,耗时4s
9.sql出错导致feign超时调用

sql错误,导致消息一直不能被消费,消费者就一直消费(.ListenerExecutionFailedException),feign调用超时出错

10.如何保证消息不丢失
  1. 交换机持久化:在上游服务配置声明交换机时的参数
  2. 队列持久化:在下游服务接收消息,在声明队列时durable参数设置
  3. 消息持久化? 这个不用配置;因为底层MessageProperties的持久化策略默认是MessageDeliveryMode.PERSISTENT,初始化时默认消息时持久化的
sentinel防服务雪崩 流量控制

限制业务访问的qps,避免服务因流量突增而故障,预防服务挂掉

熔断降级

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/721949.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存