Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 319|回复: 0

使用Redis分布式锁处理并发,解决超卖问题

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-5-7 13:30:56 | 显示全部楼层 |阅读模式

    一、使用Apache ab模拟并发压测

    1、压测工具介绍

    $ ab -n 100 -c 100 http://www.baidu.com/

    -n表示发出100个请求,-c模拟100个并发,相当是100个人同时访问。

    还可以这样写:

    $ ab -t 60 -c 100 http://www.baidu.com/

    -t表示60秒,-c是100个并发,会在连续60秒内不停的发出请求。

    使用ab工具模拟多线程并发请求,对发出负载的机器要求比较低,既不会占用很多cpu,也不会占用很多的内存,因此也是很多DDoS攻击的必备良药,不过要慎用,别耗光自己机器的资源。通常来说1000个请求,100个并发算是比较正常的模拟。

    至于工具的使用,具体见:Apache ab 测试工具使用(一)

    下载后,进入support文件夹,执行命令。

    2、并发测试

    我创建了两张表,一个商品表,一个订单记录表;
    然后写了两个接口,一个是查询商品信息,一个是下单秒杀。

    查询订单:

    image_1caapork5q212c91aschplhfop.png-48.5kB

    秒杀下单:

    image_1caapplln4pqcnk1q4fmv44vj16.png-45.3kB

    当我并发测试时:

    $ ab -n 500 -c 100 http://localhost:8080/seckill/1/

    image_1cabf0e35c361t5qltaqj03au13.png-65.1kB

    这TM肯定不行啊,这就超卖了,明明没这么多商品,结果还卖出去了。。。

    二、synchronized处理并发

    首先,synchronized的确是一个解决办法,而且也很简单,在方法前面加一个synchronized关键字。

    但是通过压测,发现请求变的很慢,因为:
    synchronized就用一个锁把这个方法锁住了,每次访问这个方法,只会有一个线程,所以这就是它导致慢的原因。通过这种方式,保证这个方法中的代码都是单线程来处理,不会出什么问题。

    同时,使用synchronized还是存在一些问题的,首先,它无法做到细粒度的控制,比如同一时间有秒杀A商品和B商品的请求,都进入到了这个方法,虽然秒杀A商品的人很多,但是秒杀B商品的人很少,但是即使是买B商品,进入到了这个方法,也会一样的慢。

    最重要的是,它只适合单点的情况。如果以后程序水平扩展了,弄了个集群,很显然,负载均衡之后,不同的用户看到的结果一定是五花八门的。

    所以,还是使用更好的办法,使用redis分布式锁。

    三、redis分布式锁

    1、两个redis的命令

    setnx key value 简单来说,setnx就是,如果没有这个key,那么就set一个key-value, 但是如果这个key已经存在,那么将不会再次设置,get出来的value还是最开始set进去的那个value.
    网站中还专门讲到可以使用!SETNX加锁,如果获得锁,返回1,如果返回0,那么该键已经被其他的客户端锁定。
    并且也提到了如何处理死锁。

    getset key value 这个就更简单了,先通过key获取value,然后再将新的value set进去。

    2、redis分布式锁的实现

    我们希望的,无非就是这一段代码,能够单线程的去访问,因此在这段代码之前给他加锁,相应的,这段代码后面要给它解锁:

    image_1cabec77q16dibn41a207mkpb19.png-80.3kB

    2.1 引入redis依赖

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

    2.2 配置redis

    spring:  redis:  host: localhost  port: 6379

    2.3 编写加锁和解锁的方法

    package com.vito.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; /** * Created by VitoYi on 2018/4/5. */ @Component public class RedisLock { Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private StringRedisTemplate redisTemplate; /** * 加锁 * @param key 商品id * @param value 当前时间+超时时间 * @return */ public boolean lock(String key, String value) { if (redisTemplate.opsForValue().setIfAbsent(key, value)) { //这个其实就是setnx命令,只不过在java这边稍有变化,返回的是boolea return true; } //避免死锁,且只让一个线程拿到锁 String currentValue = redisTemplate.opsForValue().get(key); //如果锁过期了 if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) { //获取上一个锁的时间 String oldValues = redisTemplate.opsForValue().getAndSet(key, value); /* 只会让一个线程拿到锁 如果旧的value和currentValue相等,只会有一个线程达成条件,因为第二个线程拿到的oldValue已经和currentValue不一样了 */ if (!StringUtils.isEmpty(oldValues) && oldValues.equals(currentValue)) { return true; } } return false; } /** * 解锁 * @param key * @param value */ public void unlock(String key, String value) { try { String currentValue = redisTemplate.opsForValue().get(key); if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) { redisTemplate.opsForValue().getOperations().delete(key); } } catch (Exception e) { logger.error("『redis分布式锁』解锁异常,{}", e); } } }

    为什么要有避免死锁的一步呢?
    假设没有『避免死锁』这一步,结果在执行到下单代码的时候出了问题,毕竟操作数据库、网络、io的时候抛了个异常,这个异常是偶然抛出来的,就那么偶尔一次,那么会导致解锁步骤不去执行,这时候就没有解锁,后面的请求进来自然也或得不到锁,这就被称之为死锁。
    而这里的『避免死锁』,就是给锁加了一个过期时间,如果锁超时了,就返回true,解开之前的那个死锁。

    2.4 下单代码中引入加锁和解锁,确保只有一个线程操作

    @Autowired private RedisLock redisLock; @Override @Transactional public String seckill(Integer id)throws RuntimeException { //加锁 long time = System.currentTimeMillis() + 1000*10; //超时时间:10秒,最好设为常量 boolean isLock = redisLock.lock(String.valueOf(id), String.valueOf(time)); if(!isLock){ throw new RuntimeException("人太多了,换个姿势再试试~"); } //查库存 Product product = productMapper.findById(id); if(product.getStock()==0) throw new RuntimeException("已经卖光"); //写入订单表 Order order=new Order(); order.setProductId(product.getId()); order.setProductName(product.getName()); orderMapper.add(order); //减库存 product.setPrice(null); product.setName(null); product.setStock(product.getStock()-1); productMapper.update(product); //解锁 redisLock.unlock(String.valueOf(id),String.valueOf(time)); return findProductInfo(id); } 

    这样再来跑几次压测,就不会超卖了:

    image_1cabeppmqfn11gau8gu4gn6a5m.png-56.2kB

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-16 00:24 , Processed in 0.131061 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表