前言
有一些东西需要明确
乐观锁是解决超卖问题:
悲观锁是解决一人一单问题,多个线程同时查询到没有下过单,然后都扣减了,肯定是不对的
单机模式下用synchronized即可
但是在集群模式下,需要使用分布式锁->redis setnx实现->redission实现
而异步秒杀是说,查询库存,校验一人一单,扣减库存,的过程用lua脚本实现,然后将订单信息保存到消息队列中,然后异步保存订单即可,此处使用了lua脚本
以及分布式锁保证过程是一个一个的
秒杀优惠券保存在redis中
所以用完异步秒杀之后呢,一人一单还是用redisson实现的,在保存订单信息前进行加锁,如果获取锁失败说明保存过订单了
在异步秒杀阶段,就没有乐观锁避免超卖问题了,因为包含在了lua脚本的“校验库存充足”这个阶段了,通过redis的单线程执行,一个时候只能执行一个lua脚本, 避免库存为1时多个线程同时发现库存为1,然后同时扣减,导致超卖
上海三菱
为什么用redis存储session而不是spring session?
因为我有三台机器,轮询策略下可能会把请求负载均衡到没session那台
那用spring session可以通过配置nginx避免用redis吗
用ip_hash策略
某小厂
自定义全局Id是怎么实现的
项目中全局id由三部分组成:符号位+32位时间戳+32位序列号,符号位默认为0,时间戳为当前时间以秒为单位减去一个开始时间戳,序列号存放在redis当中,的键为固定前缀拼接上当前日期,值设置为自增,这样使得不同天的序列号对应不同的key,保证id不被用完
Id为什么不会重复?不会同时有多个线程从redis中取吗?
INCR 命令是原子操作,这意味着当多个客户端同时对同一个键执行 INCR 命令时,Redis 会确保操作的原子性。
怎么用乐观锁解决超卖问题的
乐观锁其实就是不加锁,而是在更新数据时区判断数据有没有被其他线程修改,项目里的使用方式一开始是,当扣减库存时,先去判断现在数据库内的库存和先前查找到的库存值是否相同,如果不同则说明被其他线程修改过,这种方法保证了安全,但是会带来很多线程扣减失败的问题。
所以新的解决办法是,只判断库存是否大于0,如果大于0则可以扣减,否则说明超卖
ps:不会出现第二次查询>0,扣减时等于0的情况吗?
不会,因为库存实际上是存放在mysql当中的,可以保证set stock=stock-1 where id =10 and stock>0
这条语句的查询和扣减是原子性的
后来在做秒杀优化的时候将库存放到redis当中,配合lua脚本实现一人一单和超卖问题的共同解决
Redis关注取关是怎么设计的?
关注取关本身是用mysql实现的,在follow表中有user_id和follow_user_id字段,一个用户可以有多个关注他的用户,当关注时就插入一条数据,取关就删除一条数据
而共同关注是用redis的set数据类型实现,将两个人的关注分别放到set中,再使用set的求交集方法intersect,得到共同关注的人
ZSet用过吗?
用过,在项目的点赞功能中,使用zset实现点赞按时间排名功能,zset值当中的score设置为点赞时的时间,并且如果没有点赞,则存入zset,如果已经点赞则从zset中删除
苏州佰邦达
缓存穿透的原因和解决办法
原因:缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
项目里的解决办法是缓存空对象:在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的
现在的解决办法是:如果这个数据不存在,我们不会返回404 ,还是会把这个数据写入到Redis中,并且将value设置为空,欧当再次发起查询时,我们如果发现命中之后,判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据。
当然还有其他解决方案:
- 布隆过滤
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
缓存击穿的原因和解决办法
原因:一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
那么解决方案有两种:互斥锁和逻辑国企
- 互斥锁:使用互斥锁,实现当key失效时,线程只能一个一个去访问数据库,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行
互斥锁的具体解决方案:进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能查询数据库
互斥锁的具体实现;利用redis的setnx方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false,我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。那么lock就是插入这个数据,unlock就是删除这个数据
逻辑过期:我们把过期时间设置在 redis的value中,这只是逻辑上的过期时间
解决方案:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。
优劣:线程无需等待。但是在缓存重构之前,返回的都是脏数据
缓存雪崩的原因和解决办法
原因:同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决办法:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
滴滴
项目里为什么要用消息队列
因为在原本的流程当中,很多需要操作数据库的操作是串行执行的,比如说
1、查询优惠卷
2、判断秒杀库存是否足够
3、查询订单
4、校验是否是一人一单
5、扣减库存
6、创建订单,
这样十分耗时,利用消息队列,采用异步的方法,将判断是否有资格、判断库存等任务放到lua脚本当中,如果有资格,那么将这个事件push进消息队列中,用异步线程从消息队列中不停读任务并且执行,可以加快秒杀的速度。
请求很多,消息堆积处理不过来了如何应对
这个分为三种情况:
- 消息被丢弃的情况:这种方式可以采用不设置消息过期时间来处理
- 磁盘空间不足:在其他机器上创建临时的消息队列,再写一个临时的 Consumer,作为消息的中转,把消息积压队列中的消息取出来,放到临时队列里面去。
- 快速处理海量积压消息:还是采用临时队列的方式:在原本的基础上再加上一个临时队列,设置一个topic,含有多个partition,原来的consumer不再处理业务逻辑,而是仅仅像消息搬运到新的partition当中,然后由新的comsumer执行业务逻辑,加快处理速度
用户在消息堆积时以为卡了多次请求怎么处理
可以在前端阻止请求
项目都有哪些表
有blog、blog_comment,follow,seckil_voucher,voucher,shop,shop_type,sign,user,user_info,voucher_order表
表示博客,博客评论,关注,秒杀优惠券,优惠券表,商铺,商铺类型,用户,用户信息,优惠券订单表
秒杀场景下扣减库存太慢了怎么办
- 采用上面所说的异步执行秒杀逻辑的方法
- 对库存也可以进行拆分,比如将10万库存,分成100段,每段1000个库存。对应的,就有100把锁去锁这100个库存段了,可以满足100个线程同时跑。
Redis大key如何解决
大key就是指key本身过大,key中成员数过多,key中成员过大的redis的key
解决方法:
- 对bigkey进行拆分,比如一个hash结构的key,可以拆成多个hash来存储
- 直接删除bigkey,redis4.0起提供了unlink的非阻塞删除key方式
- 对value采用压缩算法,如果压缩后还是很大,那么就继续拆分
- 对失效的数据进行定期的清理
什么是热key
热key就是一个非常热销的产品,比如突然有几十万的请求去访问redis上的某个特定key。那么,这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机。
如何解决热key
方案主要有两种:
- 利用二级缓存:比如ehcache或者hashmap,都可以作为二级缓存,当发现热key之后,把热key加载到jvm中,而针对这种热key请求,会直接从jvm中获取,而不是从redis中,比如一个十万的请求,应用层有50台机器,那么每台机器备份到2000个请求,还是可以处理的
- 备份热key:就是在redis集群当中,将这个key在多个redis上都进行保存,那么当有请求过来时,从集群当中挑选一个区访问进行取值
索引优化详细讲讲
这个就是八股啊,然后背mysql的部分就行了
项目的拦截器详细讲讲
一开始的拦截器是对于用户请求,对于需要登录的路径,进行拦截,首先获取token,然后根据token到redis当中查询用户,如果用户存在,那么就放行,并且将用户信息保存到TrheadLocal当中,并且更新登录有效期
但是这样做的问题在于,这个拦截器并不会对于不用拦截的页面生效,也就是说如果用户访问了一些不需要登录的页面,那么他是不会更新有效期的
所以优化的方案是,再设置一个对所有页面都生效的拦截器,再这个拦截器当中进行获取token,查询用户如果有用户就更新有效期没有就拉倒
在原来的拦截器只做查询ThreadLocal中的用户是否存在,存在则放行,这样的操作
如何标识用户
手机号
项目的权限刷新什么意思
在访问页面时判断是否注册
更新缓存失败了怎么办
简单的方法就是直接重试,当然也可以放到mq当中异步重试,但是没必要
重试的时候,缓存中的错数据被访问多次了,怎么解决
那么可以采用其他的更新缓存策略,比如说延迟双删,先删除缓存,再更新数据库,然后休眠一段时间, 再删除缓存
第一次删除保证了,后面的线程读取,只能从数据库读取
第二步更新数据库,将数据库更新为最新的值
第三步休眠一段时间,这段时间是为了让数据库操作有足够的时间完成
第四步删除缓存,是为了将休面这段时间内,读到的脏数据被写入缓存的,删除掉
这样可以减少脏数据的读取
讲下Redis的ZSet
Zset是redis当中的SortedSet,比set多了一个score值,按score值升序排序
那么zset底层依靠的是ziplist或者skiplist实现,当如果有序集合的元素个数小于 128
个,并且每个元素的值小于 64
字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
但是其实使用skiplist,是将skiplist和dict一起进行包装,使用通dict中的entry记录key和value,使用skiplist记录score,从而通过dict进行键值的存储,通过,skiplist存储socre并进行排序和快速的查找
当使用ziplist时,逻辑是将score和element连续存储
ZSet的范围查询的时间复杂度是多少
ZRANGE
或 ZRANGEBYSCORE
,其时间复杂度是 O(log N + M),其中 N 是有序集合中的元素数量,M 是返回的元素数量。
这是因为 Redis 内部使用了跳跃表(skip list)实现有序集合,而跳跃表的查询时间复杂度是 O(log N)。在执行范围查询时,Redis首先定位到开始的元素,然后按顺序移动到结束的元素,因此时间复杂度还取决于返回的元素数量 M。
借贷宝
项目中怎么使用Redis实现登录的
mysql当中存放用户id和手机号
发送验证码的时候将手机号和验证码存到redis当中
项目里是使用hash类型来存储用户,key随机生成的登录令牌,value是由手机号等信息包装成的user对象,在登录的时候,首先提交手机号和验证码,以手机号为key读取验证码并且校验验证码,如果一致那么从mysql当中找是否有对应的用户,如果存在则登录成功,如果不存在则视为注册,创建新用户,将信息保存到数据库,再包装成user对象保存到redis,并将token返回给前端,(然后前端面每次请求都会携带这个token)流程结束。
注意:保存到threadlocal是在拦截器中做的,拦截器从redis中找是不是有用户,有就保存到threadlocal
怎么定义热点数据的?使用哪种数据结构?
热点数据在项目中就是秒杀优惠券,用string来存储
美团优选
超卖问题为何放在redis里判断
分布式锁为什么选择redission
因为redission是可重入并且提供了锁重试和看门狗机制,可以自动实现续期,然后就讲看门狗机制的实现。。。
几个问题
点赞排行用MySQL可以吗
也可以,但是我认为还是用redis更好,如果用string类型内存过大的话,可以尝试使用bitmap来保存点赞,bitmap以位的形式保存数据,可以有效的压缩空间。
或者使用redis的hyperloglog来统计点赞数量
但是随着点赞数的增加,内存一定会非常庞大,此时可以进行以下的优化方式
使用redis分片集群,实现分布式存储,将点赞信息分散到多个Redis节点上,减轻单个节点的负载压力。
设置合理的过期时间或定期清理过期的点赞数据(因为其实对于一个点赞详细来说,我们应该进行取舍,其实前端页面只需要展示部分数据,要么保存最新的一批点赞详情,要么保存一批最旧的–也就是最先点赞的人),避免占用过多的内存空间。
字节商业化技术
讲讲优惠券秒杀
每个店铺都可以发布优惠券,秒杀方面,首先是使用redis生成全局唯一id作为订单的id(具体的方式在前面总结过),然后用redis的string类型保存秒杀优惠券的信息,普通优惠券就放在mysql当中,也没有限时。用乐观锁的思想解决超卖问题(具体怎么解决的也在前面),借助redission分布式锁和lua脚本,实现秒杀优惠券的一人一单问题。
网易游戏ai平台
Redis lua脚本实现库存预验,讲一下逻辑
lua脚本在项目中主要是在异步秒杀的场景之下,去执行判断库存,判断权限,扣减库存,并发送保存订单信息的请求到消息队列中的人物
逻辑是当用户下单时,lua脚本首先判断库存是否充足,再通过sismember来判断用户是否已经下过订单,如果已经下单过,就返回,否则执行扣减库存,把用户id存放到下单了的用户set当中,再返回优惠券id,用户id,订单id存入阻塞队列中,让阻塞队列执行保存订单信息的内容。
这个功能完全可以用代码实现,你为什么采用这个方式实现?目的?
因为用lua脚本可以避免并发下的原子性问题,而且lua脚本是在redis服务端中执行的,减少了服务端和客户端交互的次数。并且可以通过一次请求传输所有命令。
Redis怎么实现分布式锁
可以使用setnx方法实现一个分布式锁,如果setnx的返回结果是1,那么就代表这个线程得到了这个锁,未得到锁的线程等待重试即可
流程是这样的:
首先线程尝试获取锁的时候,存入自己的线程标识
获取失败则等待重试
获取成功则执行业务
执行完删除之后,首先判断锁标识是不是自己的,如果是才释放锁
但是这个锁自己实现的,不是可重入的,也没有什么过期机制,比较垃圾
csdn博客上的
lua脚本是什么?为什么要用到lua脚本
轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
用lua脚本是为了保证线程安全。
Lua脚本如何保证线程安全
Lua脚本之所以可以保证线程安全,是因为我们可以把多个操作写成一个 lua 脚本,使其具备原子性,作为一个整体执行。再由于 redis 是单线程模型,不同线程的 lua 脚本是依次执行的。也就是说,只有一个线程原子性的多个操作执行完,下一个线程才可以执行。实际上也是保证了在 redis 内部不同线程操作的串行执行,从而能够解决并发安全问题。
lua脚本的优点
1.lua脚本是作为一个整体执行的,所以中间不会被其他命令插入,无需担心并发;
2.lua脚本把多条命令一次性打包,而代码实现的事务需要向Redis发送多次请求,所以可以有效减少网络开销;
3.lua脚本可以常驻在redis内存中,所以在使用的时候,可以直接拿来复用。
lua 脚本实现的原子性是假的原子性
因为当多个指令执行时,lua脚本中的一条指令报错时,后面的指令执行失败了,但是前面的指令已经成功。且不会回滚。
其中报错原因包括:
1、指令语法错误
2、语法是正确的,但是类型不对,比如对已经存在的string类型的key,执行hset等
3、服务器挂掉了,比如lua脚本执行了一半,但是服务器挂掉了
前面两者,我们可以通过仔细检查脚本逻辑,确保脚本中的所有命令都是正确的,并且按照预期的顺序执行、使用redis.pcall处理潜在的错误以及适时使用事务,可以编写出高效、可靠的Lua脚本,确保脚本的逻辑正确性和健壮性,以避免潜在的问题。
lua脚本的回滚?
在Lua脚本中,我们可以使用Redis的WATCH指令,它允许我们监视一个或多个键的变化。当我们监视的键发生变化时,Redis会立刻中断正在执行的Lua脚本,使得整个Lua脚本操作回滚,这种方式可以实现Redis事务的回滚效果。
redis缓存的数据和数据库的数据,怎么保证一致性
这个就是八股,然后看小林coding的吧
关于好友点赞你是如何实现的
好友点赞的问题应该保证一个好友一个笔记只能点赞一次。因此考虑使用Redis中的set集合实现这个功能。
在Redis中建立set集合每一个笔记对应一个set集合set集合中保存的数据就是用户点赞的用户id这样在用户点赞的时候就可以先查询一下是否点过赞如果点过赞那么就变成取消点赞的动作,将该用户的id从set当中删去,否则则将用户id记录到set集合中。
点赞排行你是如何实现的
之前的点赞是放到set集合,那么要做排行,显示最近点赞的前五个用户,需要用到sortedset数据结构,也就是说,按照score值进行排名
socre值是使用系统时间
那么查询点赞的前五个用户时只要使用opsforzset.range即可
傻逼编程满天星网站
为什么要用 ThreadLocal 存储用户信息
很多项目中需要在代码中使用当前登录用户的信息,但是又不方便把保存用户信息的session对象传来传去,这种情况下,就可以考虑使用 ThreadLocal。ThreadLocal是一个依附于本地线程的变量,按照我的理解,每次对服务器请求,都会使用到一个线程,ThreadLocal的作用就是在这个线程的使用过程中只为这个线程所用。
ThreadLocal(线程本地变量)通常理解为“采用了空间换时间的设计思想,主要用来实现在多线程环境下的线程安全和保存线程上下文中的变量”。在实际的项目开发中(比如2C APP程序的服务器端程序),通常在APP调用服务端API接口的时候,需要token(登录)验证并且在具体的方法中可能会使用到当前登录账户的更多信息(比如当前登录账户的用户ID)。以前的做法,我们喜欢把(通过token获取)用户登录信息放在http请求的request请求对象中,但是这样做是耦合性很强,比较相同的代码重复使用。这种情况使用ThreadLocal来实现会很合适。
关注功能是如何实现的
关注和取关是基于mysql实现的,follow表当中记录了用户和关注他的用户的信息,关注就添加一条,取消关注就删除一条记录即可
共同关注功能是基于set类型实现的,intersect方法
自己总结之redisson的原理部分
首先我们要知道基于setnx的分布式锁有什么问题
基于setnx实现的分布式锁存在下面的问题:
重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。
不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。
超时释放:我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患
主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。
所以redission来了
Redisson可重入原理
在 Redisson 中,可重入锁的实现是通过 Lua 脚本来完成的。
采用hash结构来存储锁和线程,大key是锁是否存在,小key是获取锁的线程,value是重入次数
该脚本首先检查锁是否存在,如果不存在,则创建锁并将当前线程标识为持有者;如果锁已存在,脚本会检查当前线程是否已经持有该锁,如果是,则增加重入次数和续约锁的过期时间。
如果锁已经存在且获取锁的线程不是当前线程,那么说明争抢锁失败了,返回当前锁的剩余有效期
整体逻辑通过 Lua 脚本的原子性操作实现,保证了可重入锁的正确获取和释放。
Redisson锁重试机制
刚刚说到如果没获取到锁,lua脚本会返回当前锁的剩余有效期
然后redisson会执行以下流程:
- 首先用最大等待时间time减去获取锁消耗了的时间,如果小于0那么就不再等待
- 如果还有时间,那么会先记录当前时间,然后订阅”释放锁”脚本当中的publish
- 如果等待结束还没有收到订阅的publish,那么就取消订阅并且,返回获取锁失败
- 如果收到了,那么还是先减去消耗的时间,再进行重试,这是第一次重试
- 如果再没成功,那么第二次重试会用获取信号量的方法
- 如果ttl小于time,那么就等待ttl,否则等待time
- 然后将上面包装成while循环,只要时间充足, 就不停重试
Redisson锁续期机制
Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。
默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。所以如果设置了这个参数就不会走看门狗了
流程如下
- 如果我们不设置leaseTime,那么redisson会将他设置为-1,检测到leaseTime为-1时,如果获取锁成功,那么就出发看门狗机制
- 看门狗内部会维护一个concurrenthashmap叫oldentry,来存放当前获取锁的线程
- 第一次获取锁会执行更新有效期操作,第二次则不会
- 更新有效期的流程:首先获取oldentry当中的缓存,如果不存在就不续期
- 如果存在,那么会执行包装好的lua脚本,并且每10秒钟检查一次调用自己,重置有效期
- 看门狗任务是在锁释放的时候进行取消
Redisson的multilock机制
此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了
使用这把锁咱们就不使用主从了,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。
当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.

消息队列部分
为什么用Stream
如果用List:缺点:
- 无法避免消息丢失
- 只支持单消费者
如果用PubSub:
缺点:
- 不支持数据持久化
- 无法避免消息丢失
- 消息堆积有上限,超出时数据丢失
Stream:
- 消息可回溯
- 一个消息可以被多个消费者读取
- 获取消息后消息存入pendinglist,处理结束确认之后才移除
- 可以阻塞读取
- 有消息漏读的风险
异步秒杀的流程
将
1、查询优惠卷
2、判断秒杀库存是否足够
3、查询订单
4、校验是否是一人一单
5、扣减库存
6、创建订单
这些过程封装在lua脚本当中,将保存订单信息的流程使用异步的方式执行
lua脚本最后将用户id,优惠券id等信息发送到Stream队列中,然后项目启动时开启一个线程,从消息队列当中读取
如果读取过程当中存在异常,那么从pendinglist当中读取,防止消息丢失
测试
做过性能测试吗
用jmeter测试过秒杀一人一单功能,创建200个线程,然后请求头只指定相同的一个请求头,作为同一个用户,执行后查看数据库看是否只下了一单