我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Redis解决高并发问题

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

@Servicepublic 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

@RestControllerpublic 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 class="lazy" data-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>

数据库发生超发现象:

在这里插入图片描述

注意:

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

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;    }

线程同步把抢购业务方法变成了单线程执行,能保证不会发生超发现象,但随着并发量增加性能下降较大。
如果还是发生超发现象 ,是电脑性能比较强 ,我的代码逻辑上还存在缺陷,高并发造成瞬间数据穿透。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fCnSBkBx-1666238246946)(模拟并发下的Redis使用.assets/image-20211222112035751.png)]

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”称为更新锁**,在数据库事务执行过程中,它会锁定查询出来的数据,其他事务不能再对其进行读写,直到该事务完成才会只放锁,这样能避免数据不一致了。

经过上述修改,并发执行后就不会超发了。
如果还是发生超发现象 ,是电脑性能比较强 ,我的代码逻辑上还存在缺陷,高并发造成瞬间数据穿透。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uuyvHEbK-1666238246947)(模拟并发下的Redis使用.assets/image-20211222112815823.png)]

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

4 方案3:“乐观锁”

(1)乐观锁的概念

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

(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”则表示数据版本号已经变更,有其他并发请求改过库存,放弃当前操作。

@Servicepublic 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());    });}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VMYzMJRX-1666238246948)(模拟并发下的Redis使用.assets/image-20211222120347556.png)]

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

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 依赖

                <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-data-redisartifactId>        dependency>

(2)修改 application.yml 配置Redis

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

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

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

@RunWith(SpringRunner.class)@SpringBootTestpublic 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) {                //读取商品库存        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中的销量                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;
}

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

来源地址:https://blog.csdn.net/qq_51307593/article/details/127424413

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Redis解决高并发问题

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java 怎样高效地解决高并发问题?(Java如何解决高并发问题)

在当今的软件开发领域,高并发是一个常见且重要的挑战。Java作为一种广泛使用的编程语言,在处理高并发场景时有着丰富的技术和策略。一、理解高并发高并发指的是系统同时处理大量请求的能力。在高并发环境下,系统需要快速响应每个请求,以
Java 怎样高效地解决高并发问题?(Java如何解决高并发问题)
Java2024-12-19

Redis+Lua解决高并发场景抢购秒杀问题

之前写了一篇PHP+Redis链表解决高并发下商品超卖问题,今天介绍一些如何使用PHP+Redis+Lua解决高并发下商品超卖问题。为何要使用Lua脚本解决商品超卖的问题呢?Redis在2.6版本后原生支持Lua脚本功能,允许开发者使用Lua语言编写脚本传到R
Redis+Lua解决高并发场景抢购秒杀问题
2017-11-10

详解利用redis + lua解决抢红包高并发的问题

抢红包的需求分析 抢红包的场景有点像秒杀,但是要比秒杀简单点。因为秒杀通常要和库存相关。而抢红包则可以允许有些红包没有被抢到,因为发红包的人不会有损失,没抢完的钱再退回给发红包的人即可。另外像小米这样的抢购也要比淘宝的要简单,也是因为像小米
2022-06-04

PHP高并发问题如何解决

PHP高并发问题如何解决?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。重启与过载保护如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起
2023-06-15

Java如何解决高并发问题

在Java中,可以采用以下几种方式来解决高并发问题:1. 使用线程池:线程池可以有效地管理线程的创建和销毁,避免频繁地创建和销毁线程,从而提高系统的并发能力。2. 使用并发集合类:Java提供了一系列的并发集合类,如ConcurrentHa
2023-10-20

PHP怎么解决高并发问题

这篇文章主要介绍了PHP怎么解决高并发问题,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1
2023-06-15

如何解决PHP高并发问题

这篇“如何解决PHP高并发问题”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“如何解决PHP高并发问题”文章吧。秒杀会产生一个
2023-06-29

golang高并发问题怎么解决

在Go语言中,高并发问题可以通过以下几种方式来解决:使用goroutine:Goroutine是Go语言的轻量级线程,可以并发执行函数。通过将任务分解为多个并发执行的goroutine,可以提高程序的并发处理能力。使用channel:Cha
golang高并发问题怎么解决
2024-02-29

redis多进程并发问题怎么解决

在处理Redis多进程并发问题时,可以考虑以下几种解决方案:使用分布式锁:可以使用Redis的SETNX命令来实现分布式锁,确保同一时刻只有一个进程能够操作共享资源。使用事务和乐观锁:通过Redis的事务和WATCH命令结合乐观锁的方式来实
redis多进程并发问题怎么解决
2024-04-09

redis单线程并发问题如何解决

Redis是一个单线程的内存数据库,它使用了事件驱动的模型,通过将请求放入一个队列中顺序执行来实现并发处理。但是,在高并发情况下,可能会出现性能瓶颈。要解决Redis的单线程并发问题,可以考虑以下几个方面:1. 水平扩展:通过使用Redis
2023-09-11

如何解决Redis高并发防止秒杀超卖的问题

这篇文章主要介绍了如何解决Redis高并发防止秒杀超卖的问题,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。1:解决思路将活动写入 redis 中,通过 redis 自减指令扣
2023-06-25

redis如何解决高并发

redis 高效解决高并发问题,主要得益于以下机制:1)内存存储,极速读写;2)单线程事件循环,无锁竞争;3)多路复用 i/o,同时监听多个连接;4)优化数据结构,快速查找、插入和删除;5)复制和集群,负载均衡和容错;6)持久化,确保数据安
redis如何解决高并发
2024-06-12

redis怎么解决高并发

redis 提供多种高并发解决方案:1. 集群分片数据,提升吞吐量和容错性;2. 复制确保数据可用性和提升读性能;3. 管道减少网络开销,提高吞吐量;4. 事务保证原子性,防止数据不一致;5. lua 脚本减少通信,提高性能;6. 发布/订
redis怎么解决高并发
2024-06-03

面试:Redis 是单线程,是怎么解决高并发问题的

Nginx 与 uWSGI 一起工作,Nginx 处理静态文件,将动态的接口请求转发给 uWSGI。这就是涉及 Nginx 与 uWSGI 以何种协议进行通信,Nginx 的 uwsgi_pass 选项告诉它使用特殊的 uWSGI 协议,而

编程热搜

目录