随着项目架构由最简单的单体结构,到后面的集群模式,再到后面的微服务架构,架构开始也越来越复杂。传统的jvm进程锁可能满足不了当前的软件架构,所以分布式锁越来越多被用到。
说起分布式锁,可能实现方式有很多,常见的可能有以下几种:
- 基于数据库
- 基于redis
- 基于zookeeper
基于数据库的设计比较简单,原理就是根据数据库唯一索引。实现方式自己可以上网搜索,本文就不着重说明。
基于redisredis为什么能做分布式锁,是因为redis是属于单线程模型,底层是因为file event handler(文件事件处理器)的设计是单线程模型。
实战演练- 新建项目
- 点击next
- 点击next
- 新建完成之后,修改配置文件
- 模拟业务代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/test")
@Slf4j
public class TestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/test")
public String test() {
String num = stringRedisTemplate.opsForValue().get("num");
int testNum = Integer.parseInt(num);
if (testNum > 0) {
log.info("库存数量为{}", testNum);
testNum--;
stringRedisTemplate.opsForValue().set("num", testNum + "");
} else {
return "库存不足";
}
return "成功";
}
}
- idea启动一个项目的两个实例(不会的可以上网查找资料)
- nginx做负载均衡(不会安装的可以上网查下资料),配置文件如下
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream mysvr {
server 127.0.0.1:8080;
server 127.0.0.1:8090;
}
server {
listen 80;
server_name localhost;
location ~*^.+$ {
proxy_pass http://mysvr;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
- 测试架构是否正常
后台日志验证负载均衡是否正常
- 8080日志
- 8090日志
- 并发问题模拟
- 首先需要在redis中将key为num的设置初始值,也就是库存量为10
- 用压测工具jmeter新建测试用例
-
添加线程组
-
添加http请求
-
添加测试结果
-
- 测试结果
- 8080
- 8090
问题分析:
- 首先需要在redis中将key为num的设置初始值,也就是库存量为10
线程1和线程2同时读取库存,同时修改库存,返回库存,肯定会出现并发问题(超卖问题)。
- 加入redis分布式锁
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/test")
@Slf4j
public class TestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/test")
public String test() throws InterruptedException {
String lKey = "order";
String uuid = UUID.randomUUID().toString();
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lKey, uuid, 1, TimeUnit.SECONDS);
if(!aBoolean){
log.warn("获取redis锁失败");
return "获取redis锁失败";
}
try {
String num = stringRedisTemplate.opsForValue().get("num");
int testNum = Integer.parseInt(num);
if (testNum > 0) {
log.info("库存数量为{}", testNum);
testNum--;
stringRedisTemplate.opsForValue().set("num", testNum + "");
} else {
log.info("库存不足");
// 为了保证释放的是自己的锁
if(uuid.equals(stringRedisTemplate.opsForValue().get(lKey))){
stringRedisTemplate.delete(lKey);
}
return "库存不足";
}
} finally {
// 为了保证释放的是自己的锁
if(uuid.equals(stringRedisTemplate.opsForValue().get(lKey))){
stringRedisTemplate.delete(lKey);
}
}
return "成功";
}
}
测试结果:
-
8080
-
8090
注意点: -
为什么要加key的过期时间?
问题场景,如果线程刚刚拿到锁,在这个时候程序被强行停止,那么这个锁将永远不会被释放,后续所有线程都将无法拿到锁。
-
为什么在释放锁的时候需要判断uuid?
问题场景,如果线程执行业务逻辑的时间比设置的时间还久,那么等线程执行到释放锁的代码的时候释放的是不是就不是自己的那把锁了,有可能就是其他线程的锁,会导致锁失效。
-
什么时候释放锁?
首先是业务正常执行完逻辑之后需要释放锁,业务逻辑出现异常也需要释放锁,库存不足了也是需要释放锁。
这样一把redis分布式锁就实现了!



