高并发场景:秒杀商品。

秒杀一般出现在商城的促销活动中,指定了一定数量(比如:1000个)的商品(比如:手机),以极低的价格(比如:0.1元),让大量用户参与活动,但只有极少数用户能够购买成功.

示例代码

@RestController
public class IndexController { @autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate @RequestMapper("/stock/deduct_stock")
public String deductStock(){ int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock") if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + ""); // jedis.set(key,value) System.out.print("======商品扣减成功,剩余库存:"+ realStock); } else {
System.out.print("======商品扣减失败======");
} return "---=== end ===---";
} }

问题

假设10个线程同时扣减1000个商品,应该剩余990 个商品,但实际扣减剩余999个,发生超卖问题。

  • 解决方案

加锁。synchronized(this)

jdk 内置锁在分布式环境下并不能保证成功。

模拟高并发场景,部署测试环境。

可用jMeter 工具进行压力测试。

添加ThreadGroup,并在栏目下添加 Http Request。配置并发计划。

可选择添加Aggregate Report 报告。

完整代码:


@RequestMapper("/stock/deduct_stock")
public String deductStock(){ synchronized(this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + ""); System.out.print("======商品扣减成功,剩余库存:"+ realStock); } else {
System.out.print("======商品扣减失败======");
} }
return "---=== end ===---";
}

启动多台服务器,测试日志结果可知,发生超卖问题。

添加分布式锁

redis 命令setnx key value

setnx 是“set if not exists” 的缩写。

含义是:

  • 将key 的值设为value, 当且仅当key 不存在。
  • 若给定的key 已经存在,则setnx 不做任何操作。

注:加锁完成后,要删除锁。

完整代码:

    String lockKey = "lock:product_100";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock,"amumu") if (!result){
return "---=== error_code ---===";
} int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + ""); System.out.print("======商品扣减成功,剩余库存:"+ realStock); } else {
System.out.print("======商品扣减失败======");
} stringRedisTemplate.delete(lockkey);
return "---=== end ===---";

分布式锁下的原子性问题优化

发现问题
  • 在set过程中程序异常,则程序无法继续执行,造成死锁。
//获取异常并抛异常
try{ } finally{ }
  • 在set 过程中,系统宕机,或系统重启。
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,"ammumu");

//添加过期时间
stringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);

上述代码执行过程中,程序异常情况下也会导致系统宕机,发生原子性问题,

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,"amumu",10, TimeUnit.SECONDS);

分布式锁的失效情况优化

  • 假设 10s 的过期时间。

假设多个请求,每个程序执行超过10s, 此时锁已过期。此时程序执行删除锁,导致此时锁失效。

分析问题:加锁过程,被其他线程执行删锁。

解决方案:加上锁标识,删除时判断是否是同一标识。

String clientId = UUID.randomUUID().toString();
// 加上cliendId 锁标识
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,clientId, 10, TimeUnits.SECONDS); //删除锁时,进行判断
if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockkey);
}
  • 假设执行删除锁前,锁快过期未过期。下一个并发线程执行加锁操作,此时上一个并发线程,执行了删除操作。锁依然会失效。

解决方案:锁续命。如redisson

参考:github.com/redisson/redisson

每隔10s 检查是否还持有锁,如果持有,则延长30s锁的时间

参考:redis lua 脚本

RLock redissonLock = redisson.getLock(lock); // 获取锁对象
redissonLock.lock(); // 加锁 相当于 setIfAbsent(lockkey,"amumu",10, TimeUnit.SECONDS); redissonLock.unlock();

完整代码:

    @autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate @RequestMapper("/stock/deduct_stock")
public String deductStock(){ String lockKey = "lock:product_101"; RLock redissonLock = redisson.getLock(lockKey);
redissonLock.lock(); // 加锁 try{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + ""); System.out.print("======商品扣减成功,剩余库存:"+ realStock); } else {
System.out.print("======商品扣减失败======");
}
}finally{
redissonLock.unlock();
} return "---=== end ===---";
}
锁续命业务优化
  • 问题1 :线程1 加锁,此时未同步给从结点,主结点挂了。此时从结点很可能会被选为主结点。这时一个线程3,同时请求此操作,此时线程3 操作新选为主结点redis 加锁,此时程序继续执行,执行有并发安全问题的代码。

  • 问题2 :redis 语义上,是把高并发场景下的问题串行化了,这当然不会有并发问题,但是对整个系统的性能有影响。当然redis 的性能已经是非常不错了。

  • 问题3:redissonLock.unLock() 底层默认的是非公平的抢锁机制,while 循环尝试去加锁。

redis 与 zookeeper 分布式集群方案对比

根据CAP 理论:

redis 集群更多是保证AP, 即可用性。

zookeeper 集群更多是保证CP, 即一致性,ZAB写数据机制保证,不会有锁丢失的问题。

redlock 实解决redis 集群环境锁丢失问题

想用redis 的高性能,但是又不想丢失锁,有很多方法。

比如 Redlock 实现。

java client 加锁三个以上的锁,超过半数redis 节点加锁成功才算加锁成功。

  • 问题:千万不要用从节点。要保证集群高可用,多加几个节点。
  • 问题:持久化。一般用aof 做持久化,假设设置1s 持久化一次。

    这样就设置一个key,设置2个key 时系统宕机或重启。client2 执行请求,key3 加锁成功,key2 重启后加锁成功,则加锁成功。系统继续执行的是并发安全问题的代码。如果用时时持久化设置,则性能会大大降低,还不如用zookeeper.

高并发分布式锁如何实现

分段加锁逻辑。

未完待续

by: 一只阿木木

生产级Redis 高并发分布式锁实战1:高并发分布式锁如何实现的更多相关文章

  1. java亿级流量电商详情页系统的大型高并发与高可用缓存架构实战视频教程

    亿级流量电商详情页系统的大型高并发与高可用缓存架构实战 完整高清含源码,需要课程的联系QQ:2608609000 1[免费观看]课程介绍以及高并发高可用复杂系统中的缓存架构有哪些东西2[免费观看]基于 ...

  2. 15套java架构师、集群、高可用、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战视频教程

    * { font-family: "Microsoft YaHei" !important } h1 { color: #FF0 } 15套java架构师.集群.高可用.高可扩展. ...

  3. java架构师负载均衡、高并发、nginx优化、tomcat集群、异步性能优化、Dubbo分布式、Redis持久化、ActiveMQ中间件、Netty互联网、spring大型分布式项目实战视频教程百度网盘

    15套Java架构师详情 * { font-family: "Microsoft YaHei" !important } h1 { background-color: #006; ...

  4. 15套java架构师、集群、高可用、高可扩 展、高性能、高并发、性能优化Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战视频教程

    * { font-family: "Microsoft YaHei" !important } h1 { color: #FF0 } 15套java架构师.集群.高可用.高可扩 展 ...

  5. java-spring基于redis单机版(redisTemplate)实现的分布式锁+redis消息队列,可用于秒杀,定时器,高并发,抢购

    此教程不涉及整合spring整合redis,可另行查阅资料教程. 代码: RedisLock package com.cashloan.analytics.utils; import org.slf4 ...

  6. 基于Redis实现分布式锁实战

    背景在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等.大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端 ...

  7. 15套java互联网架构师、高并发、集群、负载均衡、高可用、数据库设计、缓存、性能优化、大型分布式 项目实战视频教程

    * { font-family: "Microsoft YaHei" !important } h1 { color: #FF0 } 15套java架构师.集群.高可用.高可扩 展 ...

  8. Java 架构师+高并发+性能优化+Spring boot大型分布式项目实战

    视频课程内容包含: 高级 Java 架构师包含:Spring boot.Spring cloud.Dubbo.Redis.ActiveMQ.Nginx.Mycat.Spring.MongoDB.Zer ...

  9. Redis分布式锁实战

    什么是分布式锁 在单机部署的情况下,要想保证特定业务在顺序执行,通过JDK提供的synchronized关键字.Semaphore.ReentrantLock,或者我们也可以基于AQS定制化锁.单机部 ...

  10. Redsi缓存问题(穿透,击穿,雪崩)以及解决办法(分布式锁)【高并发问题】

    Redsi常见问题 缓存在高平发和安全压力下的一些问题 缓存击穿 是某一个热点key在高并发访问的情况下,突然失效,导致大量的并发大金mysql数据库的情况 缓存穿透 是利用redis和mysql的机 ...

随机推荐

  1. 国内版Unity 2023编辑器无法切换到DX12模式的解决方法

    2024.7.4更新:至6000.0.5f1c1,此问题已修复 在{安装目录}\Editor里(也就是Unity安装的根目录)建立一个名叫D3D12的文件夹. 去{安装目录}\Editor\Data\ ...

  2. [oeasy]python0141_自制模块_module_reusability_复用性

    自制包内容 回忆上次内容 上次导入了外部的py文件 import my_module 导入一个自己定义的模块   可以使用my_module中的变量 不能 直接使用 my_module.py文件中的变 ...

  3. [oeasy]python0013_ASCII码表_英文字符编码_键盘字符

    ​ ASCII 码表 回忆上次内容 ​ord(c)​​和​​chr(i)​ 这是俩函数 这俩函数是一对,相反相成的⚖️ ​​ord​​ 通过 ​​字符​​ 找到对应的 ​​数字​​ ​​chr​​ 通 ...

  4. 一种优秀的虚拟机内存架构 - AQ

    源链接:https://www.axa6.com/zh/an-excellent-virtual-machine-memory-architecture 简介 虚拟机内存架构直接影响虚拟机的性能和占用 ...

  5. LLM并行训练6-激活优化

    前置知识 Activation 激活指的是一些在fp时计算得到的临时tensor, 会用于bp时的计算. 如果能在fp计算后把临时tensor缓存下来就可以加速bp, 缺点在于某些激活会占用大量显存. ...

  6. Python 基于pymongo操作Mongodb学习总结

    实践环境 Python 3.6.4 pymongo 4.1.1 pymongo-3.12.3-cp36-cp36m-win_amd64.whl 下载地址:https://pypi.org/simple ...

  7. MySQL原始密码登录出现错误

    1.首先查看自己的MySQL安装目录下有没有data文件夹,和bin目录是同级的.要是有就删除,然后执行下列操作.没有就直接执行操作: 2. 以管理员身份运行 cmd.遇到个同学,可能我强调的不够明显 ...

  8. java spring boot 2 开发实战 mybtis 基础部份从搭建到第一个完整测试(从环境到测试用例二部份)

    本案例是java  sping boot  2.2.1 mybtis 基础部份 第一步搭建环境:安装依赖 由于我们公司项目是1.8 环境不能乱,我现在自己的电脑是1.8环境,所以本次整理的boot 代 ...

  9. Java SE 文件上传和文件下载的底层原理

    1. Java SE 文件上传和文件下载的底层原理 @ 目录 1. Java SE 文件上传和文件下载的底层原理 2. 文件上传 2.1 文件上传应用实例 2.2 文件上传注意事项和细节 3. 文件下 ...

  10. mysql数据库无法录入汉字问题

    1.插入数据出现错误 show full columns from 表名;//查看数据表列编码 2. alter table 表名 change 列名 列名 varchar(自己设置) charact ...