文章目录
- 一、引入
- 一个程序的线程问题
- 多个程序的线程问题
- 二、什么是分布式锁
- 分布式锁常见实现
- 为什么使用redis作为分布式锁
- redisson实现分布式锁的原理
- 三、不加分布式锁的问题出现
- 3.1 创建springboot的项目,引入依赖
- 3.2 代码
- 3.3 Jmeter测试结果
- 四、加入redisson分布式锁
- 4.1 伪代码
- 4.2 代码
- 4.3 Jmeter测试结果
- 尾语
一、引入
作为编程人员,需要关注的一个事情,就是并发运行带来的问题
注意:分布式锁也是不可避免的事情,但是目前作为分布式锁的组件,主推redis,这是一个非常常见的分布式锁,也是程序员必须掌握的。
并发编程需要注意的:
- 可见性:一个线程对共享变量的修改,另一个线程能立刻看到。缓存可导致可见性问题。
- 原子性:一个或多个CPU执行操作不被中断。线程切换可导致原子性问题。
- 有序性:编译器优化可能导致指令顺序发生改变。编译器优化可能导致有序性问题。
一个程序的线程问题
如下图,多个线程去抢占一个资源:
解决方法,对线程加锁。
总结来说,Lock与synchronized有以下区别:
- Lock是一个接口,而synchronized是关键字。
- synchronized会自动释放锁,而Lock必须手动释放锁。
- Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。
- 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
- Lock能提高多个线程读操作的效率。
- synchronized能锁住类、方法和代码块,而Lock是块范围内的
多个程序的线程问题
在高可用的环境下,就会出现多个程序抢占一个资源的问题,普通的代码锁明显是无法解决这个问题,这个时候就必须采用分布式锁。
如下图,采用redis作为分布式锁:
二、什么是分布式锁
在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
- 高可用、高性能的获取锁与释放锁; (高可用)
1、锁的颗粒度要尽量小
2、锁的范围尽量要小 - 具备可重入特性;(重入)
- 具备锁失效机制、防止死锁;(过期)
- 具备非阻塞锁特性,即没有获取到锁直接返回获取锁失败; (互斥)
分布式锁常见实现
分布式锁的实现又哪些?
1数据库层面的乐观锁;
2基于zookeeper的节点的实现;
3基于redis中间件的实现
性能比较:redis中间件>zookeeper=数据库
可靠性比较:zookeeper>redis中间件>数据库
为什么使用redis作为分布式锁
相信各位小伙伴在学习Redis时,都了解到Redis不仅仅是一个内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
redis常见客户端:
Redis官方对Java 语言的封装框架推荐的有十多种(Redis 官网),主要是Jedis 、Redisson。
根据redis官网 https://redis.io/clients#java 所言,redisson是使用在 分布式和可伸缩的Java数据结构在Redis服务器上的
redisson实现分布式锁的原理
- 加锁机制
线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。 - 为啥要用lua脚本呢?
这个不用多说,主要是如果你的业务逻辑复杂的话,通过封装在lua脚本中发送给redis,而且redis是单线程的,这样就保证这段复杂业务逻辑执行的原子性。
三、不加分布式锁的问题出现
3.1 创建springboot的项目,引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springboot中的redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.9.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2 代码
- Controller
@RestController
public class NoDistributeController {
/**
* 无分布式锁
*/
@Autowired
NoDistributeService redisDistributeService;
/**
* 查询剩余订单结果接口
* @return
*/
@GetMapping("/query")
public String query() {
return redisDistributeService.queryMap();
}
/**
* 下单接口
* @return
*/
@GetMapping("/order")
public String order() {
redisDistributeService.order();
return redisDistributeService.queryMap();
}
}
- Service
@Service
public class NoDistributeService {
/**
* 模拟商品信息表
*/
private static Map<String,Integer> products;
/**
* 模拟库存
*/
private static Map<String,Integer> stock;
/**
* 订单
*/
private static Map<String,String> orders;
static {
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
//模拟订单表数据 订单编号 苹果 库存 100000
products.put("苹果",100000);
//模拟库存表数据 订单编号 苹果 库存100000
stock.put("苹果",100000);
}
/**
* 模拟查询秒杀成功返回的信息
* @return 返回拼接的秒杀商品结果字符串
*/
public String queryMap() {
String pid = "苹果";
return "秒杀商品限量:" + products.get(pid) + "份,还剩:"+stock.get(pid) +"份,成功下单:"+orders.size() + "人";
}
/**
* 下单
*/
public void order() {
String pid = "苹果";
//从库存表中获取库存余量
int stockNum = stock.get(pid);
//如果库存为0 则输出库存不足
if(stockNum == 0) {
System.out.println("商品库存不足");
}else{ //如果有库存
//往订单表中插入数据 生成UUID作为用户ID pid
orders.put(UUID.randomUUID().toString(),pid);
//线程休眠 模拟其他操作
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//减库存操作
stock.put(pid,stockNum-1);
}
}
}
3.3 Jmeter测试结果
2秒,下单1000次
查看结果:http://localhost:8080/query
可以看出, 下单了994个人,而订单只下了29份,严重出现问题。
四、加入redisson分布式锁
4.1 伪代码
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // cluster state scan interval in milliseconds
.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
.addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");
// traditional lock method
//lock方法是直接加锁,如果锁已被占用,则直接线程阻塞,进行等待,直到锁被占用方释放。
lock.lock();
// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);
// or wait for lock aquisition up to 100 seconds
// and automatically unlock it after 10 seconds
//tryLock方法则是设定了waitTime(等待时间),在这个等待时间没到前,也是线程阻塞并反复去获取锁,直到取到锁或等待时间超时,则返回false。
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
更多加锁方法可以参考 https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers
4.2 代码
package com.dislock.redis.lock;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
public class NoDistributeService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 模拟商品信息表
*/
private static Map<String, Integer> products;
/**
* 模拟库存
*/
private static Map<String, Integer> stock;
/**
* 订单
*/
private static Map<String, String> orders;
/**
* 获取锁
*/
private static Redisson redisson;
static {
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
//模拟订单表数据 订单编号 苹果 库存 100000
products.put("苹果", 100000);
//模拟库存表数据 订单编号 苹果 库存100000
stock.put("苹果", 100000);
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
redisson = (Redisson) Redisson.create(config);
}
/**
* 模拟查询秒杀成功返回的信息
*
* @return 返回拼接的秒杀商品结果字符串
*/
public String queryMap() {
String pid = "苹果";
return "秒杀商品限量:" + products.get(pid) + "份,还剩:" + stock.get(pid) + "份,成功下单:" + orders.size() + "人";
}
/**
* 下单
*/
public void order() {
String pid = "苹果";
//从库存表中获取库存余量
int stockNum = stock.get(pid);
// 获得锁对象实例
RLock lock = redisson.getLock("lock_Key");
boolean res = false;
try {
res = lock.tryLock();
//locked = lock.tryLock(1,2,TimeUnit.MINUTES);
if (res) {
try {
//如果库存为0 则输出库存不足
if (stockNum == 0) {
System.out.println("商品库存不足");
} else { //如果有库存
//往订单表中插入数据 生成UUID作为用户ID pid
orders.put(UUID.randomUUID().toString(), pid);
//线程休眠 模拟其他操作
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//减库存操作
stock.put(pid, stockNum - 1);
}
} finally {
logger.info("释放锁");
lock.unlock();
}
}
} catch (Exception e) {
logger.info("获取锁有误");
}
}
}
4.3 Jmeter测试结果
结果下单的人和剩余的人准确无误。