赞
踩
2023-7-31 16:01:39
以下内容源自《【面试复习】》
仅供学习交流使用
禁止其他平台发布时删除以下此话
本文首次发布于CSDN平台
作者是CSDN@日星月云
博客主页是https://blog.csdn.net/qq_51625007
禁止其他平台发布时删除以上此话
无
用户:
前端请求后端:获取验证码
// 生成OTP
UUID:4位的随机数(0~9)
// 绑定OTP
2用户注册与登录:后端:把验证码存入到session中
6分布式状态管理:存入到redis中的<phone,otp,5mins>
// 发送OTP
输出到控制台里
改为短信发送
短信验证码【java提高】 https://jsss-1.blog.csdn.net/article/details/125790714
用户填写信息,点击立即注册
前端请求后端:注册
UserController:调用register()
// 验证OTP
2用户注册与登录:session.getAttribute()
6分布式状态管理:取到phone对应的otp
// 加密处理
// 注册用户
用户输入手机号和密码
前端请求后端:登录
后端调用login()
校验用户名和密码
2用户注册与登录:存入到session中
6分布式状态管理:存入到redis中的token中 返回token给前端
前端:把返回的token存入到浏览器sessionStorage
每次ajax异步请求都会带上token,为了得到当前登录用户
调用UserController:getUser()
2用户注册与登录:从session中取出
6分布式状态管理:在redis中取出
https://juejin.cn/post/6844903975800537096
本质就是存在于浏览器上的 hash(哈希表)。
localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。存放数据大小为一般为5MB,而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信。
sessionStorage 的所有性质基本上与 localStorage 一致,唯一的不同区别在于:
sessionStorage 的有效期是页面会话持续,如果页面会话(session)结束(关闭窗口或标签页),sessionStorage 就会消失。而 localStorage 则会一直存在。
cookie和session及token,为什么选择使用token?JWT使用
https://blog.csdn.net/weixin_44211968/article/details/128112325
序列化:把对象转化为可传输的字节序列过程称为序列化。
反序列化:把字节序列还原为对象的过程称为反序列化。
Java项目笔记之秒杀接口防刷限流
https://www.shangyexinzhi.com/article/2741037.html
RocketMQ和其他消息中间件最大的一个区别是支持了事务消息,
这也是分布式事务里面的基于消息的最终一致性方案。
在redis中预减缓存,防止超卖
mq保证mysql中库存最终被扣减
redis存储库存,mq保证最终一致性
OrderController.create()
抛出:下单失败
OrderServiceImpl.createOrderAsync()
如果已经售罄了:hasKey()
抛出:已经售罄
生成库存流水:createItemStockLog()
输出:生成库存流水完成
输出:投递扣减库存事务消息
发送事务消息:sendMessageInTransaction
抛出:创建订单失败
LocalTransactionListenerImpl.executeLocalTransaction()
执行本地事务:createOrder()
抛出:执行MQ本地事务时发生错误
LocalTransactionListenerImpl.createOrder()
输出:本地事务提交完成
return RocketMQLocalTransactionState.COMMIT;
抛出:创建订单失败&更新流水完成
return RocketMQLocalTransactionState.ROLLBACK;
OrderServiceImpl.createOrder()
预扣库存:decreaseStockInCache()
**ItemServiceImpl.decreaseStockInCache()**
在redis中实现:decrement
库存为0是添加售罄标识 `<"item:stock:over:" + itemId, 1>`
输出:回补库存完成|售罄标识完成
return result>=0;
输出:预扣减库存完成
抛出:库存不足
生成订单
输出:生成订单完成
更新销量,发送异步消息
输出:投递增加商品销量消息成功|输出:投递增加商品销量消息失败
{
**IncreaseSalesConsumer.onMessage()**
itemService.increaseSales(itemId, amount);
输出:更新销量完成|更新销量失败
}
更新库存流水的状态
输出:更新流水完成
DecreaseStockConsumer.onMessage()
itemService.decreaseStock(itemId, amount)
输出:最终扣减库存完成|输出:从DB扣减库存失败
点击商品之后,查询缓存(会初始化的)
点击购买输入验证码(redis验证码 1min)
redis验证码 1 min
提交验证码
判断验证码是否正确
限流
获取秒杀凭证(先判断有没有售罄标识 redis decrement)
获取不到,下单失败
防刷
RateLimiter;一个限制访问速率的工具
判断售罄标识
生成库存流水
执行本地事务
预减库存(redis decrement)
生成订单
检查本地事务(查看库存流水表)
执行本地事务
扣减库存
下单成功
//预扣库存 //redisTemplate.opsForValue().decrement(key, amount); //售罄标识 //redisTemplate.opsForValue().set("item:stock:over:" + itemId, 1); @Override public boolean decreaseStockInCache(int itemId, int amount) { if (itemId <= 0 || amount <= 0) { throw new BusinessException(PARAMETER_ERROR, "参数不合法!"); } String key = "item:stock:" + itemId; long result = redisTemplate.opsForValue().decrement(key, amount); if (result < 0) { // 回补库存 this.increaseStockInCache(itemId, amount); logger.debug("回补库存完成 [" + itemId + "]"); } else if (result == 0) { // 售罄标识 redisTemplate.opsForValue().set("item:stock:over:" + itemId, 1); logger.debug("售罄标识完成 [" + itemId + "]"); } return result >= 0; }
在上述代码中,使用Redis的decrement操作来预扣库存。如果库存数量减为负数(result < 0),则说明库存不足,需要回补库存。如果库存数量减至0(result == 0),则设置售罄标识。
这段代码可以一定程度上缓解超卖问题,但并不能完全解决。在高并发场景下,多个线程可能会同时进行decrement操作,导致库存数量减为负数。
可以通过以下方式来进一步解决超卖问题:
使用悲观锁或乐观锁来保证同一时间只有一个线程能够进行decrement操作,避免超卖问题。
在decrement操作前,通过GET命令获取当前库存数量,然后进行判断。如果库存数量小于等于请求的数量,则不进行decrement操作,避免超卖问题。
使用Lua脚本来确保decrement操作的原子性,同时进行判断和减少库存数量,避免并发问题。
综上所述,单纯使用Redis的decrement操作无法完全解决超卖问题,需要结合其他策略来确保库存的准确性和一致性。
//使用Lua脚本 //得到stock //判断stock>amount //DECRBY stockKey, amount //SET stockOverKey, 1 @Override public boolean decreaseStockInCache(int itemId, int amount) { if (itemId <= 0 || amount <= 0) { throw new BusinessException(PARAMETER_ERROR, "参数不合法!"); } String script = "local itemId = ARGV[1]\n" + "local amount = tonumber(ARGV[2])\n" + "local stockKey = \"item:stock:\" .. itemId\n" + "local stockOverKey = \"item:stock:over:\" .. itemId\n" + "local stock = tonumber(redis.call(\"GET\", stockKey))\n" + "if stock == nil or stock < amount then\n" + " return 0\n" + "else\n" + " local result = redis.call(\"DECRBY\", stockKey, amount)\n" + " if result == 0 then\n" + " redis.call(\"SET\", stockOverKey, 1)\n" + " end\n" + " return 1\n" + "end"; List<String> keys = new ArrayList<>(); keys.add("item:stock:" + itemId); keys.add("item:stock:over:" + itemId); Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, itemId, amount); return result != null && (Long) result == 1; }
使用Lua脚本可以解决一些并发操作的问题,特别是在Redis中。由于Redis的单线程特性,执行Lua脚本可以保证原子性操作,并避免了多个命令的竞态条件。
在预扣库存的场景中,使用Lua脚本可以确保以下几点:
原子性操作:Lua脚本在Redis中被作为一个整体进行执行,中间不会被其他操作打断,从而保证了预扣库存操作的原子性。
避免超卖:通过在Lua脚本中检查库存数量,并在预扣成功后减少库存,可以避免超卖问题的发生。
但是需要注意的是,即使使用Lua脚本也不能完全解决所有并发问题。在高并发场景下,可能仍然存在一些问题,比如竞争条件和网络延迟等。因此,在实际应用中,还需要结合其他技术和策略来提高系统的并发性和可靠性,例如分布式锁、分布式事务等。
上述代码是一个使用Lua脚本来实现分布式锁和解决超卖问题的例子。具体步骤如下:
首先,定义了一个Lua脚本,脚本中包含了获取库存、检查库存是否足够、扣减库存和标记库存是否售罄的逻辑。
在Java代码中,通过RedisTemplate的execute方法来执行Lua脚本。在执行过程中,需要传入脚本、键列表和参数。
脚本:使用DefaultRedisScript指定脚本和返回值类型。
键列表:包含了需要操作的键,这里是"item:stock:“+itemId和"item:stock:over:”+itemId。
参数:itemId和amount,即商品ID和购买数量。
执行结果返回一个Object对象,需要根据具体业务逻辑进行判断。这里判断如果返回的结果不为空,且结果等于1,表示扣减库存成功;否则表示库存不足或者其他错误。
需要注意的是,在实际应用中,还需要考虑到异常处理、分布式锁的释放、并发情况下的处理等问题。此处的代码只是一个简化的示例,具体的实现方式需要根据具体业务需求和实际情况进行调整和完善。
//1.定义一个公共的方法来获取分布式锁。 private boolean acquireLock(String lockKey, String requestId, int expireTime) { boolean lockAcquired = false; try { lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS); logger.info("获取分布式锁成功"); } catch (Exception e) { // 锁获取失败的处理逻辑 logger.info("获取分布式锁失败"); return false; } return lockAcquired; } //2.在方法中添加获取分布式锁的逻辑。 @Override public boolean decreaseStockInCache(int itemId, int amount) { if (itemId <= 0 || amount <= 0) { throw new BusinessException(PARAMETER_ERROR, "参数不合法!"); } String lockKey = "item:lock:" + itemId; String requestId = UUID.randomUUID().toString(); int expireTime = 10000; // 锁的过期时间,单位为毫秒 boolean lockAcquired = acquireLock(lockKey, requestId, expireTime); if (!lockAcquired) { // 获取锁失败的处理逻辑 logger.info("获取分布式锁失败"); return false; } try { // 执行库存扣减的逻辑 String script = "local itemId = ARGV[1]\n" + "local amount = tonumber(ARGV[2])\n" + "local stockKey = \"item:stock:\" .. itemId\n" + "local stockOverKey = \"item:stock:over:\" .. itemId\n" + "local stock = tonumber(redis.call(\"GET\", stockKey))\n" + "if stock == nil or stock < amount then\n" + " return 0\n" + "else\n" + " local result = redis.call(\"DECRBY\", stockKey, amount)\n" + " if result == 0 then\n" + " redis.call(\"SET\", stockOverKey, 1)\n" + " end\n" + " return 1\n" + "end"; List<String> keys = new ArrayList<>(); keys.add("item:stock:" + itemId); keys.add("item:stock:over:" + itemId); Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, itemId, amount); return result != null && (Long) result == 1; } finally { // 释放锁 boolean releaseLock = releaseLock(lockKey, requestId); if (releaseLock){ logger.info("释放分布式锁成功"); }else { logger.info("释放分布式锁失败"); } } } //3.定义一个公共的方法来释放分布式锁。 private boolean releaseLock(String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('del', KEYS[1]) " + "else " + " return 1 " + "end"; List<String> keys = new ArrayList<>(); keys.add(lockKey); Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, requestId); return result != null && (Long) result == 1; }
上面设置了10s
需要确保RedisLock过期时间大于业务执行时间的问题
设置分布式锁的过期时间是否合理取决于你的具体需求和业务场景。一般来说,过期时间需要根据以下因素进行考虑:
业务处理时间:你需要根据业务处理的预估时间来设置过期时间。如果你的业务处理时间通常在几秒钟内完成,可以设置一个稍微长一点的过期时间,以免在处理期间锁已经过期失效。
并发访问量:如果同时对某个资源进行大量的并发操作,你可能需要更短的过期时间,以便尽快释放锁,让其他请求能够获得锁。
锁争用频率:如果对某个资源的锁争用频率很高,建议设置较短的过期时间,以便其他请求能够更快地获取到锁。
根据你的描述,如果你的业务处理时间在几秒钟内,而且对资源的锁争用频率不是特别高,10秒的过期时间可能是一个合理的选择。但请注意,在实际应用中,你可能需要根据实际情况进行调试和优化,以找到最适合你的业务场景的过期时间设置。
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
@Configuration @PropertySource("classpath:application.properties") public class RedisConfiguration { @Autowired private Environment env; @Bean public Redisson redisson() { Config config = new Config(); String redisHost = env.getProperty("spring.redis.host"); String redisPort = env.getProperty("spring.redis.port"); String redisPassword = env.getProperty("spring.redis.password"); int redisDatabaseNumber = env.getProperty("spring.redis.database", Integer.class, 0); String redisAddress = "redis://" + redisHost + ":" + redisPort; config.useSingleServer().setAddress(redisAddress).setPassword(redisPassword).setDatabase(redisDatabaseNumber); return (Redisson)Redisson.create(config); } }
@Override public boolean decreaseStockInCache(int itemId, int amount) { if (itemId <= 0 || amount <= 0) { throw new BusinessException(PARAMETER_ERROR, "参数不合法!"); } String lockKey = "item:lock:" + itemId; String REDIS_LOCK = lockKey+":"+UUID.randomUUID().toString(); RLock redissonLock = redisson.getLock(REDIS_LOCK); redissonLock.lock(); logger.debug("redissonLock加锁成功"); try { // 执行库存扣减的逻辑 String script = "local itemId = ARGV[1]\n" + "local amount = tonumber(ARGV[2])\n" + "local stockKey = \"item:stock:\" .. itemId\n" + "local stockOverKey = \"item:stock:over:\" .. itemId\n" + "local stock = tonumber(redis.call(\"GET\", stockKey))\n" + "if stock == nil or stock < amount then\n" + " return 0\n" + "else\n" + " local result = redis.call(\"DECRBY\", stockKey, amount)\n" + " if result == 0 then\n" + " redis.call(\"SET\", stockOverKey, 1)\n" + " end\n" + " return 1\n" + "end"; List<String> keys = new ArrayList<>(); keys.add("item:stock:" + itemId); keys.add("item:stock:over:" + itemId); Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, itemId, amount); logger.debug("执行库存扣减的逻辑成功"); return result != null && (Long) result == 1; } finally { // 释放锁 //避免IllegalMonitorStateException: attempt to unlock lock,not locked by current thread by node id if (redissonLock.isLocked()){ if (redissonLock.isHeldByCurrentThread()){ redissonLock.unlock(); logger.debug("redissonLock释放成功"); } } } }
在上述代码中,通过使用分布式锁确保了库存扣减的原子性操作,从而避免了多线程环境下的超卖问题。当多个线程同时执行库存扣减操作时,只有一个线程能够获取到锁,其他线程会在redissonLock.lock()处阻塞等待。
在获取到锁后,通过Lua脚本执行库存扣减的逻辑,并且使用DECRBY命令原子性地减少库存数量。如果库存不足,返回0表示扣减失败,否则返回1表示扣减成功。在扣减成功后,通过SET命令将标识库存已售罄的键设置为1。
这种方式可以有效地解决超卖问题。但是需要注意的是,使用分布式锁会增加系统的复杂性和延迟,并且需要保证Redis服务器的可用性和性能。因此,在设计系统时需要权衡分布式锁的使用场景和影响,确保在高并发环境下依然能够保持系统的稳定性和性能。
另外,除了使用分布式锁来解决超卖问题,还可以考虑其他方案,如使用乐观锁或悲观锁、使用消息队列等。具体选择哪种方案取决于系统的需求和特点。
2023-8-17 17:14:14
Redisson是一个基于Redis的分布式Java对象和服务库,它提供了多种分布式功能,包括分布式锁。Redisson的分布式锁机制主要基于Redis的特性和命令实现。
Redisson分布式锁的原理如下:
客户端通过Redisson框架的API请求获取锁。
Redisson将客户端的请求转化为Lua脚本,并通过eval命令发送给Redis服务器。
Redis服务器在单线程下执行Lua脚本,保证了脚本的原子性和线程安全性。
Lua脚本在Redis服务器中执行以下操作:
首先,通过SET命令尝试将锁对应的键设置为指定值(通常是一个唯一标识符)。
如果设置成功,则表示获取锁成功,返回成功标识。
如果设置失败,则表示锁已被其他客户端占用,返回失败标识。
同时,设置锁的过期时间,防止锁无限占用。
Redisson客户端接收到Redis服务器返回的结果,根据结果判断锁的获取是否成功,如果成功则继续执行业务逻辑,否则等待或进行其他处理。
Redisson的分布式锁实现利用了Redis的原子操作,通过设置指定的值来实现锁的获取和释放。同时,使用Lua脚本可以保证获取锁的操作是原子的,避免了竞争条件和并发问题。
需要注意的是,Redisson的分布式锁还具备可重入性和红锁特性,使得锁的使用更加灵活和可靠。
Redisson分布式锁相对于直接使用SETNX
命令实现分布式锁具有以下几个优势:
可重入性:Redisson分布式锁支持可重入,也就是同一线程可以多次获取同一个锁而不会被阻塞。这种可重入性能够解决一些特定场景下的并发问题。
锁的自动续期:Redisson分布式锁支持锁的自动续期,即在获取锁后,锁的过期时间会不断更新,防止持有锁的客户端在执行业务逻辑时由于各种原因导致锁过期而被其他客户端抢占。
防止死锁的解锁机制:Redisson分布式锁通过监听锁的失效事件来实现解锁操作,避免了因为客户端崩溃或其他原因导致的锁无法释放的情况。
红锁特性:Redisson分布式锁支持红锁特性,即在多个Redis节点之间实现锁的互斥。当多个Redis节点之间进行通信时,可以使用分布式锁的红锁特性保证在大多数节点上加锁成功才算真正获取到锁,避免了分布式环境下的数据不一致问题。
强大的扩展性:Redisson分布式锁是基于Redisson框架的分布式Java对象和服务库实现的,提供了很多其他功能和特性,如分布式服务、分布式集合等。使用Redisson分布式锁可以很方便地与其他分布式功能进行集成和扩展。
综上所述,Redisson分布式锁相比于直接使用SETNX
命令实现分布式锁,具有更多的功能和特性,可以更方便地应对分布式环境中的并发和竞争问题。
对用户注册登录的密码使用公私钥加密进行前后端传输
对密码进行MD5+盐值存入数据库
sms验证码登录
秒杀链接动态化
订单超时未支付,取消订单,回补库存
超卖:分布式锁
订单号生成使用雪花算法
设计一个秒杀系统,需要考虑这些问题:
如何解决这些问题呢?
秒杀活动的页面,大多数内容都固定不变,如商品名称,商品图片等等,可以对活动页面做静态化处理,减少访问服务端的请求。秒杀用户会分布在全国各地,有在上海,有在深圳,地域相差很远,网速也各不相同。为了让用户最快访问到活动页面,可以使用 CDN(Content Delivery Network,内容分发网络)。CDN 可以让用户就近获取所需内容。
秒杀活动开始前,按钮一般需要置灰的。只有时间到了,才能变得可以点击。这样防止,秒杀用户在时间快到时前几秒,疯狂请求服务器,然后秒杀时间点还没到,服务器就自己挂了。
我们都知道微服务设计思想,也就是把各个功能模块拆分,功能那个类似的放
一起,再用分布式的部署方式。
如用户登录相关的,就设计个用户服务,订单相关就搞个订单服务,再到礼物相关的就搞个礼物服务等等。那么,秒杀相关业务逻辑也可以放到一起,搞个秒杀务,单独给它搞个秒杀数据库。
服务单一职责有个好处:如果秒杀没抗住高并发的压力,秒杀库崩了,服务挂了,也不会影响到系统的其他服务。
链接如果明文暴露的话,会有人获取到请求 Url,提前秒杀了。因此,需要给秒杀链接加盐。可以使URL 动态化,如通过 MD5 加密算法加密随机字符串去做 url。
一般有两种方式限流:nginx 限流和redis 限流。
可以使用 redis 分布式锁解决超卖问题。
使用 Redis 使用 SET EX PX NX + 校验唯一随机值,再删除释放锁。
if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1{ //加锁
try {
do something / / 业务处理
}catch(){
}
finally {
//判断是不是当前线程加锁,才释放
if (uni_request_id.equals(jedis.get(key_resource_id))) {
jedis.del(lockKey); //释放锁
}
}
}
在这里,判断是不是当前线程加锁和释放锁不是一个原子操作。如果调用
jedis.del()释放锁时候,可能这锁已经不属于当前客户端,会解除他人加
锁。
为了更严谨,一般也✁用 lua 脚本代替。lua 脚本如下:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
如果瞬间流量特别大,可以使用消息队列削峰,异步处理。用户请求过来✁时
候,先放到消息队列,再拿出来消费。
所谓限流,就是指限制流量请求的频次。它主要是在高并发情况下,用于保护系统的一种策略,主要是避免在流量高峰导致系统崩溃,造成系统不可用的问题。
实现限流常见的算法4种,分别是计数器限流算法、滑动窗口限流算法、漏桶限流算法、令牌桶限流算法。下面,我给大家详细介绍每种算法的基本原理。
1、计数器限流算法。
一般用在单一维度的访问频率限制上,比如短信验证码每隔 60s只能发送一次,或者接口调用
次数等。它的实现方法很简单,就是每调用一次就加 1,处理结束以后减1。
2、滑动窗口限流算法,
本质上也是一种计数器,只是通过以时间为维度的可滑动窗口设计,来减少了临界值带来的并发超过阈值的问题。每次进行数据统计的时候,只需要统计这 个窗口内每个时间刻度的访问量就可以了。
Spring Cloud 中的熔断框架 Hystrix,以及 Spring Cloud Alibaba 中的Sentinel 都采用滑动窗口来做数据统计。
3、漏桶限流算法。
它是一种恒定速率的限流算法,不管请求量是多少,服务端的处理效率是恒定的。基于 MQ 来实现
的生产者消费者模型,其实算是一种漏桶限流算法。
4、令牌桶限流算法。
相对漏桶算法来说,它可以处理突发流量的问题。它的核心思想是,令牌桶以恒定速率去生成令牌保存到令牌桶里面,桶的大小是固定的,令牌桶满了以后就不再生成令牌。
每个客户端请求进来的时候,必须要从令牌桶获得一个令牌才能访问,否则排队等待。在流量低峰的时候,令牌桶会出现堆积,因此当出现瞬时高峰的时候,有足够多的令牌可以获取,因此令牌桶能够允许瞬时流量的处理。
网关层面的限流、或者接口调用的限流,都可以使用令牌桶算法,像 Google 的Guava,和Redisson 的限流,都用到了令牌桶算法。
我认为,限流的本质是实现系统保护,最终选择什么样的算法,一方面取决于统计的精准度,另一方面考虑限流维度和场景的需求。
2023-7-31 16:02:02
我们都有光明的未来
祝大家考研上岸
祝大家工作顺利
祝大家得偿所愿
祝大家如愿以偿
点赞收藏关注哦
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。