SpringBoot之使用Redis实现分布式锁(秒杀系统)
作者:水能载舟,亦能覆舟 发布时间:2022-09-30 15:45:08
一、Redis分布式锁概念篇
建议直接采用Redis的官方推荐的Redisson作为redis的分布式锁
1.1、为什么要使用分布式锁
我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程的18般武艺进行处理,并且可以完美的运行,毫无Bug!
注意这是单机应用,也就是所有的请求都会分配到当前服务器的JVM内部,然后映射为操作系统的线程进行处理!而这个共享变量只是在这个JVM内部的一块内存空间!
后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:
上图可以看到,变量A存在JVM1、JVM2、JVM3三个JVM内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象,例如:UserController控制器中的一个整形类型的成员变量),如果不加任何控制的话,变量A同时都会在JVM分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同JVM内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!
如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!
为了保证一个方法或属性在高并 * 况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
1.2、分布式锁应具备哪些条件
在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
1.3、分布式锁的三种实现方式
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。
1、基于数据库实现分布式锁;
2、基于缓存(Redis等)实现分布式锁;
3、基于Zookeeper实现分布式锁;
尽管有这三种方案,但是不同的业务也要根据自己的情况进行选型,他们之间没有最好只有更适合!
二、Redis分布式锁实战篇
2.1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
2.2、配置Redis配置信息
spring
redis:
port: 6379
host: 127.0.0.1
password:
database: 0
2.3、配置RedisConfig属性、如果需要使用FastJSON来序列化你的对象可以看看我前面写的一篇文章
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate initRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws Exception {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2.4、写一个RedisLock工具类
/**
* @Description //直接使用Redis进行分布式锁
* 这是简易版本 如果要使用Redis原生锁记得加过期时间,防止死锁 最好使用Redisson操作简单更加方便
* @Date $ $
* @Author huangwb
**/
@Component
public class RedisLockCommon {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* Redis加锁的操作
*
* @param key
* @param value
* @return
*/
public Boolean tryLock(String key, String value) {
if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
String currentValue = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isNotEmpty(currentValue) && Long.valueOf(currentValue) < System.currentTimeMillis()) {
//获取上一个锁的时间 如果高并发的情况可能会出现已经被修改的问题 所以多一次判断保证线程的安全
String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
if (StringUtils.isNotEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}
return false;
}
/**
* Redis解锁的操作
*
* @param key
* @param value
*/
public void unlock(String key, String value) {
String currentValue = stringRedisTemplate.opsForValue().get(key);
try {
if (StringUtils.isNotEmpty(currentValue) && currentValue.equals(value)) {
stringRedisTemplate.opsForValue().getOperations().delete(key);
}
} catch (Exception e) {
}
}
}
2.5、减库存操作
@Override
public boolean decrementProductStore(Long productId, Integer productQuantity) {
String key = "dec_store_lock_" + productId;
long time = System.currentTimeMillis();
try {
//如果加锁失败
if (!redisLock.tryLock(key, String.valueOf(time))) {
return false;
}
ProductInfo productInfo = productInfoMapper.selectByPrimaryKey(productId);
//如果库存为空
if (productInfo.getProductStock() == 0) {
return false;
}
//减库存操作
productInfo.setProductStock(productInfo.getProductStock() - 1);
productInfoMapper.updateByPrimaryKey(productInfo);
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
//解锁
redisLock.unlock(key, String.valueOf(time));
}
return true;
}
2.6、测试接口
@GetMapping("test")
public String createOrderTest() {
if (!productInfoService.decrementProductStore(1L, 1)) {
return "库存不足";
}
OrderMaster orderMaster = new OrderMaster();
//未支付
orderMaster.setOrderStatus(0);
//未支付
orderMaster.setPayStatus(0);
orderMaster.setBuyerName("张三");
orderMaster.setBuyerAddress("湖南长沙");
orderMaster.setBuyerPhone("18692794847");
orderMaster.setOrderAmount(BigDecimal.ZERO);
orderMaster.setCreateTime(DateUtils.getCurrentDate());
orderMaster.setOrderId(UUID.randomUUID().toString().replaceAll("-", ""));
orderMasterService.insert(orderMaster);
}
2.7、可以使用使用Apache ab进行http接口测试
具体文章可以看看这篇文章 https://www.jb51.net/article/177250.htm
三、结尾
文章概览篇来源于 https://blog.csdn.net/hxpjava1/article/details/81068355
来源:https://blog.csdn.net/qq_37892957/article/details/89322334


猜你喜欢
- 今天来了一个问题:软键盘无法弹出。分析后是因为系统判断当前有外接硬键盘,就会隐藏软键盘。但实际情况并不是这么简单,该问题只有在特定条件下偶现
- 目录一、图示二、链表的概念及结构三、单链表的实现四、完整代码的展示一、图示二、链表的概念及结构 链表是一种物理存储结构上非连续存储结构,数据
- ForkJoin简介Fork/Join框架是Java 7提供的一种用于并行执行任务的框架,它将大任务分解为若干个小任务,并行执行这些小任务,
- 脚本之家在以前介绍过关于C#创建、部署、调用WebService的教程,有兴趣的可以参阅:.NET C#创建WebService服务简单实例
- 一个真实的故事大学的时候就开过一门课程,讲设计模式,可是大学生没什么编程实践经验,在大学里面听设计模式的感觉,就像听天书。听着都有道理,可是
- package com.letv.cloud.spider;import java.util.HashSet;import java.uti
- 1. pom.xml文件配置<?xml version="1.0" encoding="UTF-8&qu
- 在使用spring框架中我们都知道,某个类如果使用了@Service、@Autowire 这种依赖注入的方式引用了其他对象,在另外一个类中,
- 通常,在这个页面中会用到很多控件,控件会用到很多的资源。Android系统本身有很多的资源,包括各种各样的字符串、图片、动画、样式和布局等等
- 本文实例讲述了从C#程序中调用非受管DLLs的方法。分享给大家供大家参考。具体方法如下:前言:从所周知,.NET已经渐渐成为一种技术时尚,那
- Android SimpleAdapter使用详解HolderAdapter背景Android的AdapterView用的比较多,ListV
- 背景传说里玉皇大帝派龙王马上降雨到共光一带,龙王接到玉皇大帝命令,立马从海上调水,跑去共光施云布雨,但粗心又着急的龙王不小心把海里的鲸鱼随着
- 引言在学习Java过程中,排序sort是我们常用的功能;在Java里,数组有Arrays.sort()可以排序,集合则是Collection
- Glide是一款基于Android的图片加载和图片缓存组件,它可以最大性能地在Android设备上读取、解码、显示图片和视频。Glide可以
- 建立Android项目,如果会的话特别简单,不会的话让自己去琢磨也需要一定的时间!小编之后将自己学习Android的经验给大家分享出来!1、
- Spire.Cloud.SDK for .NET提供了接口PdfSecurityApi可用于加密、解密PDF文档。本文将通过C#代码演示具体
- 最近项目中用到了service进行计时,在连接USB的情况下一切正常,但是拔掉USB后发现,手机进入休眠后service停止了工作。最后通过
- 本文所述代码是从一个C#程序中摘录出来的,主要实现C#创建目录、删除文件夹及文件的功能,包括所引用的名称空间等,特别是对C#不熟悉的初学者,
- 引言native image是GraalVM中提供的一个命令,可以把字节码文件或Jar包编译成为一个二进制可执行文件,同时它自己也是用Jav
- 本文实例为大家分享了C# SqlHelper应用技巧,供大家参考,具体内容如下使用App.config配置文件封装连接字符串,方便重复使用-