Redis解决高并发问题

这篇具有很好参考价值的文章主要介绍了Redis解决高并发问题。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1 模拟商品抢购和并发的效果

这里模拟一个商品抢购的过程所带来的问题,以及解决问题的思路。

这里模拟的商品抢购过程是一个商品正常购买的过程,其中包含了两个主要的步骤:商品库存减少和商品购买记录的添加。

下面搭建项目环境。

1.1 数据库结构(MySQL)
DROP DATABASE IF EXISTS rush_to_purchase_db;
CREATE DATABASE rush_to_purchase_db;
USE rush_to_purchase_db;

/* 产品信息表 */
CREATE TABLE t_product(
    id INT(12) NOT NULL AUTO_INCREMENT COMMENT '商品编号',
    NAME VARCHAR(60) NOT NULL COMMENT '商品名称',
    stock INT(10) NOT NULL COMMENT '库存',
    price DECIMAL(16,2) NOT NULL COMMENT '单价',
    VERSION INT(10) NOT NULL DEFAULT 0 COMMENT '版本号',
    note VARCHAR(256) NULL COMMENT '备注',
    PRIMARY KEY(id)
);
/* 购买信息表 */
CREATE TABLE t_purchase_record(
  id INT(12) NOT NULL AUTO_INCREMENT COMMENT '记录编号',
  userId INT(12) NOT NULL  COMMENT '用户编号',
  productId INT(12) NOT NULL  COMMENT '商品编号',
  price DECIMAL(16,2) NOT NULL COMMENT '价格',
  quantity INT(12) NOT NULL  COMMENT '数量',
  purchaseTime TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '购买时间',
  note VARCHAR(512) NOT NULL COMMENT '备注',
  PRIMARY KEY(id)
);
INSERT INTO t_product VALUES(1, 'Yogas2020笔记本电脑', 50, 4000, DEFAULT, 'Yogas2020笔记本电脑,14.3寸,轻便之选');
1.2 创建SpringBoot的SSM项目,实现基本购物功能

(1)Model

public class Product implements Serializable {
    private int id;
    private String name;
    private int stock;
    private double price;
    private int version;
    private String note;
    //省略getter、setter
}
public class PurchaseRecord implements Serializable {
    private int id;
    private int userId;
    private int productId;
    private double price;
    private int quantity;
    private double totalPrice;
    private Timestamp purchaseTime;
    private String note;
 	//省略getter、setter   
}

(2)Mapper

public interface ProductMapper {
    @Select("SELECT id,name,stock,price,VERSION,note FROM t_product where id=#{id}")
    Product selectById(long id);

    @Update("update t_product set stock=stock- #{quantity} where id=#{id}")
    void descreaseStock(long id, long quantity);
}

public interface PurchaseRecordMapper {
    @Options(keyProperty = "id", useGeneratedKeys = true)
    @Insert("INSERT INTO t_purchase_record(userId,productId,price,quantity,purchaseTime,note) VALUES(#{userId},#{productId},#{price},#{quantity},#{purchaseTime},#{note})")
    void insert(PurchaseRecord record);
}

(3)Service

@Service
public class PurchaseServiceImpl implements PurchaseService {
    @Autowired
    private PurchaseRecordMapper purchaseRecordMapper;
    @Autowired
    private ProductMapper productMapper;

    @Transactional
    public boolean purchase(int userId, int productId, int quantity) {
        //根据产品id判断库存是否够
        Product product= productMapper.selectById(productId);
        //如果库存不够,购买失败
        if(quantity>product.getStock()) {
            return false;
        }
        //如果库存足够--减库存
        productMapper.descreaseStock(productId,quantity);
        //增加购买记录
        addPurchaseRecord(userId,product,quantity);
        return true;
    }
    //添加购买记录
    private void addPurchaseRecord(int userId, Product product, int quantity){
        PurchaseRecord record=new PurchaseRecord();
        record.setPrice(product.getPrice());
        record.setPurchaseTime(new Timestamp(System.currentTimeMillis()));
        record.setProductId(product.getId());
        record.setUserId(userId);
        record.setNote("购买时间:"+System.currentTimeMillis());
        purchaseRecordMapper.insert(record);
    }
}

(4)Controller

@RestController
public class PurchaseController {
    @Autowired
    private PurchaseService purchaseService;
    @PostMapping("/api/purchase")
    public String purchase(int userId,int productId,int quantity){
        boolean flag=purchaseService.purchase(userId,productId,quantity);
        return flag?"抢购成功":"抢购失败";
    }
}

(4)index.html:使用jQuery Ajax模拟抢购过程

<script src="jquery.js"></script>
<script>
    $(function(){
        //抢购按钮模拟500人抢购50台笔记本
        $("#rush2buy").click(function(){
            for(var i=1; i<=500; i++){
                var params = {userId:1, productId:1, quantity: 1};
                $.post("api/purchase", params, function(result){
                    console.log(new Date().getTime());
                });
            }
        });
    })
</script>

数据库发生超发现象:

Redis解决高并发问题

注意:

如果是低并发量测试一般时没问题的,如果购买不成功有正确的提示,如果是高并发量就会出现超发现象,即库存小于0的问题。即库存原本只有50,但是500个人去抢的时候,最后库存变成了-3,相当于卖出了53台.

Redis解决高并发问题

2 方案1:线程同步

上述的超发现象,归根到底在于数据库时被多个线程同时访问的,在没有加锁的情况下,上述代码并不是线程安全的。

最简单的办法是为业务方法添加线程同步“synchroized”关键字,确保同一个时间只有一个线程进入操作。

    //添加synchronized实现线程同步
    @Transactional
    public synchronized boolean purchase(int userId, int productId, int quantity) {
        //根据产品id判断库存是否够
        Product product= productMapper.selectById(productId);
        //如果库存不够,购买失败
        if(quantity>product.getStock()) {
            return false;
        }
        //如果库存足够--减库存
        productMapper.descreaseStock(productId,quantity);
        //增加购买记录
        addPurchaseRecord(userId,product,quantity);
        return true;
    }

线程同步把抢购业务方法变成了单线程执行,能保证不会发生超发现象,但随着并发量增加性能下降较大。
如果还是发生超发现象 ,是电脑性能比较强 ,我的代码逻辑上还存在缺陷,高并发造成瞬间数据穿透。
Redis解决高并发问题

3 方案2:数据库“悲观锁”

高并发情况下出现的问题,主要原因在于共享资源(stock)被多个线程并行修改。

如果一个数据库事务读取到产品库存后,就直接把该行数据锁定,不允许其它事务读写,直到事务完成商品库存的减少在释放锁,就不会出现并超发现象了。MySQL就提供数据库锁的解决方案,这种锁称为悲观锁。具体操作如下:

修改上述Mapper中的查询语句,在每次查询商品库存的时候加上更新锁。

public interface ProductMapper {
    @Select("SELECT id,name AS productName,stock,price,VERSION,note FROM t_product where id=#{id} for update")
    Product selectById(long id); 
    ......
}

注意上述语句中“SELECT id,product_name AS productName,stock,price,VERSION,note FROM t_product where id=#{id} for update” 中的**“for update”称为更新锁**,在数据库事务执行过程中,它会锁定查询出来的数据,其他事务不能再对其进行读写,直到该事务完成才会只放锁,这样能避免数据不一致了。

经过上述修改,并发执行后就不会超发了。
如果还是发生超发现象 ,是电脑性能比较强 ,我的代码逻辑上还存在缺陷,高并发造成瞬间数据穿透。
Redis解决高并发问题

但由于加锁,会导致实际代码的执行时间有所增加。

4 方案3:“乐观锁”

(1)乐观锁的概念

悲观锁虽然可以解决高并发下的超发现象,却并非高效方案,另一些开发者会采用乐观锁方案。乐观锁并非数据库加锁和阻塞的解决方案,乐观锁把读取到的旧数据保存下来,等到要对数据进行修改的时候,会先把旧数据与当前数据库数据进行比较,如果旧数据与当前数据一致,我们就认为数据库没有被并发修改过,否则就认为数据已经被其它并发请求修改,当前的事务回滚,不再修改任何数据。在实际操作中,乐观锁通常需要在数据表中增加“数据版本号”这样一个字段,以标识当前数据和旧数据是否一致,每次修改数据后“数据版本号”要增加。

Redis解决高并发问题

(2)乐观锁的使用

修改减少库存的Mapper方法,每次减少库存的时候同时修改数据的版本号version

public interface ProductMapper {
    //不使用悲观锁
    @Select("SELECT id,name AS productName,stock,price,VERSION,note FROM t_product where id=#{id}")
    Product selectById(long id);

    //使用乐观锁(添加版本号条件和版本号增加)
    @Update("update t_product set stock=stock- #{quantity}, version=version+1 where id=#{id} and version=#{version}")
    void descreaseStock(long id, long quantity, long version);
}

修改业务方法,每次修改库存时检查是否修改到,如果没改到数据“result==0”则表示数据版本号已经变更,有其他并发请求改过库存,放弃当前操作。

@Service
public class PurchaseServiceImpl implements PurchaseService {
    ......
    //乐观锁方案
    @Transactional
    public boolean purchase(int userId, int productId, int quantity) {
        //根据产品id判断库存是否够
        Product product=productMapper.selectById(productId);
        //如果库存不够,购买失败
        if(quantity>product.getStock()) {
            return false;
        }
        //如果库存足够--减库存
        int result=productMapper.descreaseStock(productId,quantity,product.getVersion());
        // 影响行数0,没修改成,代表版本号已经改变,已经并发,放弃本次修改
        System.out.println(result);
        if(result==0) {
            return false;
        }
        //增加购买记录
        addPurchaseRecord(userId,product,quantity);
        return true;
    }
}

乐观锁可以很好的提高执行效率,也可以确保不会出现超发的数据不一致问题。但是,乐观锁也有自己的问题,请求失败率变得很高,以致数据库可能还有剩余的商品。

例如,我们把模拟的抢购人数从500将为100,则可能看到库存还有剩余商品。

for(var i=1; i<=100; i++){	//将为100
    var params = {userId:1, productId:1, quantity: 1};
    $.post("api/purchase", params, function(result){
        console.log(new Date().getTime());
    });
}

Redis解决高并发问题

因此,乐观锁虽然能避免并发,却并不适合抢购的业务场景,当然,我们也可以增加失败重试的机制去增加成功率。

5 方案4:使用Redis提高并发性

实际中,引入Redis这类NoSQL是提高并发性的更好选择。

Redis这类的NoSQL数据库以Hash的方式把数据存放在内存中,在理想环境中每秒读写次数可以高达10万次,数据吞吐效率远高于SQL数据库,因此常常用来解决大规模并发的访问效率问题。

Redis 的“INCR”命令可以将key中存储的数字值加1。如果key不存在,那么Key的值会先被初始化为0,然后再执行INCR操作。Redis 中的该操作是原子性的,不会被高并发打断,可以确保数据的一致性。

5.1 使用Redis计数器的处理思路:

(1)抢购开始前,Redis缓存抢购商品的HashMap:从数据库中读取参加抢购的商品(ID)和对应的库存(stock)保存在Redis中;

(2)Redis中为每件抢购商品单独保存一个计数器:key中保存商品id信息,value中保存商品的销量(sales);

(3)处理每个购买请求时,先从Redis中读取商品库存(stock)和之前的销量(sales);若“库存<之前销量+本次购买量” 则 返回购买失败;否则 使用原子计数器增加销量,并继续执行后续的数据库操作;

5.2 具体实现:

(1)为Spring Boot 项目引入 Redis 依赖

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

(2)修改 application.yml 配置Redis

spring:
  #redis配置连接
  redis:
    database: 0
    host: localhost
    port: 6379

(4)在开始抢购前缓存商品和库存集合

这里为了方便测试,直接用SpringBootTest来模拟把商品预先缓存到Redis的操作。

@RunWith(SpringRunner.class)
@SpringBootTest
public class AddStocks2RedisTests {
    @Autowired
    @Qualifier("stringRedisTemplate")
    private RedisTemplate redisTemplate;
    @Autowired
    private ProductService productService;
    
    @Test
    public void testAddStocks2Redis() {
        productService.findAll().forEach(x->{
            redisTemplate.opsForHash().put("product-stocks", x.getId()+"", x.getStock()+"");
        });
        redisTemplate.expire("product-stocks", 3600, TimeUnit.SECONDS);
    }
}

(4)重写PurchaseServiceImpl中的purchase方法,处理购买前先检查Redis中的库存和销量

    @Autowired
    @Qualifier("stringRedisTemplate")
    private RedisTemplate<String,String> redisTemplate;

    //使用Redis判断库存量是否够发,过滤掉超发请求,然后再进行SQL操作
    @Transactional
    public boolean purchase(int userId, int productId, int quantity) {
        /* 使用Redis对比并发时的销量和库存量是否一致,排除超发请求 */
        //读取商品库存
        long stock = Long.parseLong(redisTemplate.opsForHash().get("product-stocks", productId + "").toString());
        //读取商品销量
        String value = redisTemplate.opsForValue().get("product-sales-" + productId);
        int sales = 0;
        if (value != null) {
            sales = Integer.valueOf(value);
        } else {    //若还没有对应产品的销量,向Redis初始化该产品销量为0
            redisTemplate.opsForValue().set("product-sales-" + productId, "0", 3600, TimeUnit.SECONDS);
        }
        if (stock < (sales + quantity)) {   //对比库存量和销量,库存不足销售时返回false
            return false;
        }
        redisTemplate.opsForValue().increment("product-sales-" + productId, quantity);  //增加Redis中的销量

        /* 以下为MySQL数据库操作,不超发的请求才执行,可以保留悲观锁操作,以防Redis中还可能有漏网的并发  */
        Product product = productMapper.selectById(productId);
        if (product.getStock() < quantity) {
            return false;
        }
        //减少库存
        productMapper.descreaseStock(productId, quantity);
        //增加购买记录
        addPurchaseRecord(userId,product,quantity);
        return true;
    }

这个方法利用了Redis的高速访问特性,有效的提高了并发超发的检查效率。

下为MySQL数据库操作,不超发的请求才执行,可以保留悲观锁操作,以防Redis中还可能有漏网的并发 */
Product product = productMapper.selectById(productId);
if (product.getStock() < quantity) {
return false;
}
//减少库存
productMapper.descreaseStock(productId, quantity);
//增加购买记录
addPurchaseRecord(userId,product,quantity);
return true;
}文章来源地址https://www.toymoban.com/news/detail-484537.html


这个方法利用了Redis的高速访问特性,有效的提高了并发超发的检查效率。

在实际应用中,我们还可以把购买的整个过程使用Redis操作记录下来,在空闲的时候再把结果同步回SQL数据库,这样就真的能解决并发的效率问题了。

到了这里,关于Redis解决高并发问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 高性能商品秒杀抢购系统

    完整资料进入【数字空间】查看——baidu搜索\\\"writebug\\\" Go+iris+rabbbitmq+mysql构建高性能商品秒杀抢购系统 1. 课程目标 应用GoWeb快速构建秒杀系统 全流程应用开发及架构化设计思维梳理 逐级优化,轻松应对“秒杀”及类似高并发场景 2. 知识储备 RabbitMQ入门 Iris入门 3. 基础功能开发

    2024年02月11日
    浏览(44)
  • JMeter压力测试案例(商品超卖并发问题)

    压力测试可以用来验证软件系统的稳定性和可靠性,在压力下测试系统的性能和稳定性,发现并解决潜在的问题,确保系统在高负载情况下不会崩溃。 压力测试可以用来评估软件系统的容量和性能,通过模拟高负载情况下的用户访问量和数据量,测试系统的处理能力和响应速

    2024年02月05日
    浏览(40)
  • Redis的内存淘汰策略有哪些?Redis的发布订阅功能是如何实现的?如何监控Redis的性能?Redis的并发竞争问题如何解决?

    Redis的内存淘汰策略有以下几种: noeviction :不进行任何内存淘汰,当内存用完时,新的写操作将会返回错误。 volatile-lru :在所有已设置过期时间的键中,使用近似LRU算法删除最长时间未使用的键,直到腾出足够的内存空间为止。 volatile-ttl :在所有已设置过期时间的键中,

    2024年02月12日
    浏览(83)
  • 使用python脚本抢购天猫和淘宝商品,0秒下单

    最近想抢购一下淘宝和天猫的秒杀商品,md,老是抢不过别人,所以去github上找了一份抢购的代码。根据自己的需要,修改了一下。 最终的效果如下所示: 使用python脚本抢购天猫和淘宝商品,0秒下单 主要的实现思路 1. 使用selenium的debuggerAddress模式创建一个新的chrome浏览器,

    2024年02月11日
    浏览(46)
  • chatgpt赋能python:使用Python来进行抢购,如何轻松秒杀商品

    在现代社会,大量的人们使用电商平台来完成他们的购物需求。然而,在疯狂的购物节日时,例如\\\"双11\\\",商品往往会被抢购一空。为此,许多人希望能够使用自动化脚本,在秒级别内完成商品的购买,以避免人工购物时的抢购高峰。 Python是一种优秀的编程语言,因为它可以

    2024年02月07日
    浏览(51)
  • 【Redis】Redis 高并发常见问题

    目前市场上许多软件产品,其内部都采用 Redis 作为数据缓存的主要解决方案。随着业务的不断发展,在高并发场景里,Redis 常常会出现一些问题,网络上也有很多文章对其做出了总结。这里主要针对 缓存穿透、缓存雪崩、缓存击穿 这几类场景进行分析,并提出可能的处理方

    2023年04月24日
    浏览(38)
  • redis的并发安全问题:redis的事务VSLua脚本

    在redis中,处理的数据都在内存中,数据操作效率极高,单线程的情况下,qps轻松破10w。反而在使用多线程时,为了保证线程安全,采用了一些同步机制,以及多线程的上下文切换,却对性能造成了一定的影响。 如此看来,在单线程模式下,redis的性能比较高,且可以避免多

    2024年02月14日
    浏览(30)
  • 秒杀抢购案例,基于 Redis 实现

    目录 1、关于全局唯一 ID 生成器 1.1 需要满足的特性 1.2 代码实现 1.3 其他的唯一 ID 生成策略 2、实现秒杀下单 2.1 超卖问题的产生 2.2 超卖问题的分析与解决 2.21 悲观锁与乐观锁  2.22 乐观锁中的两种常用方案        ▶️version 版本控制方案 ▶️CAS方案 2.3 实现一人一单 2.4

    2024年02月08日
    浏览(44)
  • python selenium 模拟浏览器自动操作抢购脚本

    每逢秒杀,都在遗憾网速和手速慢没能抢购到商品吧。 手写一个脚本,让程序帮你抢,抢到的概率会大大提升。 废话不多说,直接上代码。 本实例以华为官网抢购手机为例

    2024年02月09日
    浏览(42)
  • 【Redis】电商项目秒杀问题之下单接口优化:Redis缓存、MQ以及lua脚本优化高并发背景下的秒杀下单问题

    目录 一、优化思路 二、缓存库存与订单 1、库存缓存的redis数据结构 2、订单信息缓存的redis数据结构 三、整体流程 四、lua脚本确保权限校验操作的原子性 【Redis】电商项目秒杀问题之超卖问题与一人一单问题_1373i的博客-CSDN博客 https://blog.csdn.net/qq_61903414/article/details/1305689

    2024年02月05日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包