关于分布式锁,适用于并发量特别大的微服务集群,能做到同步的实现资源的获取
我其实没有经过真实项目的分布式锁的实践,以下的作为我学习的参考,但据我了解一般使用
redis
作为分布式锁的公司具体实现也如我下述的redisson
框架,只要不是像淘宝、京东那样并发量特别高的项目都基本适用,如果以后有机会使用到了分布式锁的应该场景我也会更新本文我会从分布式锁的原理解析、代码、框架一一解析,本文解析代码部分仅供参考
原理
redis> SETNX mykey "Hello" (integer) 1 redis> SETNX mykey "World" (integer) 0 redis> GET mykey "Hello"
如上面代码所示
setnx
请求是指,先查找查找是否有mykey
这个key
,如果没有则放入一个Hello
的value
并返回1
(就是表名插入成功),如果已经存在mykey
这个key
则返回0
(表示插入失败)由于redis是单线程的所有在redis方法会让进来的请求进行排队,下面用户一的请求比用户二的请求快一丢丢访问redis,实现分布式锁
用户一在服务一使用
setnx
这个方法插入一个key
返回成功,并表示用户一拿到分布式锁同一时间用户二在服务二使用
setnx
这个方法插入一个相同的key
,这时提示插入失败,返回失败,然后服务二就自旋拿锁或者直接返回用户稍后重试(直接返回用户不友好)之后用户一处理完请求,修改redis数据,最后把
key
删除掉,这样其他的服务就可以拿到锁了,就可以使用setnx
命令尝试加锁,拿到锁后操作redis
的数据,拿不到锁的执行上一个步骤(这里不是有三个点吗,就是执行第二个点后面的内容)
SpringBoot
作为实现分布式的基本框架,只跑单个服务的时候,使用的是一个jvm
来控制代码。我们假设一个秒杀的项目实现:
@RestController
public class DemoController {
@Autowired
private RedisUtil redisUtil;
@RequestMapping("/demo")
public String demo(){
try {
// 取goods的值
Integer goods = (Integer) redisUtil.get("goods");
if (goods <= 0){
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "商品已经取完");
return "商品已经取完";
}
// 这里模拟一下延时 0.1秒 (因为数据量太少,这样可以很直观的看出加锁和不加锁的区别)
Thread.sleep(100);
// 用户拿到了这个商品,所以这个商品需要自减一
// 使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
// 由于用户已经取到了商品,所以redis中的数据也需要更新
redisUtil.set("goods",goods);
} catch (InterruptedException e) {
return "错误";
}
return "你已经成功获取商品";
}
}
一瞬间有20个用户去抢goods
这个商品
按理说,每个用户只抢一个商品,那么会剩余30个商品,我们来看看下述情况,发现全部的用户都拿到的是第50这个数据,所以减1操作,后都在redis中存储的是49,就造成了数据的脏读,而修改此处代码非常的简单
只需要修改代码,加入锁就可以了,这样就实现了基于jvm层面的加锁了
@RestController
public class DemoController {
@Autowired
private RedisUtil redisUtil;
@RequestMapping("/demo")
public String demo(){
try {
// 加锁
synchronized (this){
// 取goods的值
Integer goods = (Integer) redisUtil.get("goods");
if (goods <= 0){
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "商品已经取完");
return "商品已经取完";
}
// 这里模拟一下延时 0.1秒 (因为数据量太少,这样可以很直观的看出加锁和不加锁的区别)
Thread.sleep(100);
// 用户拿到了这个商品,所以这个商品需要自减一
// 使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
// 由于用户已经取到了商品,所以redis中的数据也需要更新
redisUtil.set("goods",goods);
}
} catch (InterruptedException e) {
return "错误";
}
return "你已经成功获取商品";
}
}
测试 场景一 4 的代码
会出现怎样的错误,记得修改redis中的goods数据至50(以下操作不知道怎么处理的,可以看需要用到的知识点)worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 负载均衡配置访问路径 serverList名字随便取
upstream serverList{
# 这个是tomcat的访问路径
server localhost:8080;
server localhost:9090;
}
server {
listen 80;
server_name localhost;
location / {
root html;
proxy_pass http://serverList;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
让两个服务分别处理25个请求
后面有很多两个项目取出了相同的商品,我这里就不拉开展示了,知道有问题就好了
@RestController
public class DemoController {
@Autowired
private RedisUtil redisUtil;
@RequestMapping("/demo")
public String demo(){
// 设置自旋超时时间
long timeoutAt = System.currentTimeMillis();
try {
// 自旋
while (true){
long now = System.currentTimeMillis();
// 5秒超时,退出
if (now - timeoutAt > 5000){
System.out.println("连接超时请重试");
return "连接超时请重试";
}
// 加锁
boolean lock = redisUtil.setnx("lock", "先随便输入什么东西都可以啦~");
if (!lock){
continue;
}
// 取goods的值
Integer goods = (Integer) redisUtil.get("goods");
if (goods <= 0){
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "商品已经取完");
return "商品已经取完";
}
// 这里加了锁就不需要
// Thread.sleep(100);
// 用户拿到了这个商品,所以这个商品需要自减一
// 使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
// 由于用户已经取到了商品,所以redis中的数据也需要更新
redisUtil.set("goods",goods);
// 解锁
redisUtil.delete("lock");
return "你已经成功获取商品";
}
} catch (Exception e) {
return "错误";
}
}
}
@RestController
public class DemoController {
@Autowired
private RedisUtil redisUtil;
@RequestMapping("/demo")
public String demo(){
// 设置自旋超时时间
long timeoutAt = System.currentTimeMillis();
// 自旋
while (true){
long now = System.currentTimeMillis();
// 5秒超时,退出
if (now - timeoutAt > 5000){
System.out.println("连接超时请重试");
return "连接超时请重试";
}
// 加锁
boolean lock = redisUtil.setnx("lock", "先随便输入什么东西都可以啦~");
if (!lock){
continue;
}
try {
// 取goods的值
Integer goods = (Integer) redisUtil.get("goods");
if (goods <= 0){
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "商品已经取完");
return "商品已经取完";
}
// 用户拿到了这个商品,所以这个商品需要自减一
// 使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
// 由于用户已经取到了商品,所以redis中的数据也需要更新
redisUtil.set("goods",goods);
return "你已经成功获取商品";
} catch (Exception e) {
e.printStackTrace();
return "请重试";
} finally {
// 解锁
redisUtil.delete("lock");
}
}
}
}
@RestController
public class DemoController {
@Autowired
private RedisUtil redisUtil;
@RequestMapping("/demo")
public String demo(){
// 设置自旋超时时间
long timeoutAt = System.currentTimeMillis();
// 自旋
while (true){
long now = System.currentTimeMillis();
// 5秒超时,退出
if (now - timeoutAt > 5000){
System.out.println("连接超时请重试");
return "连接超时请重试";
}
// 加锁,并设置缓存时间 10秒
boolean lock = redisUtil.setnx("lock", "先随便输入什么东西都可以啦~",10, TimeUnit.SECONDS);
if (!lock){
continue;
}
try {
// 取goods的值
Integer goods = (Integer) redisUtil.get("goods");
if (goods <= 0){
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "商品已经取完");
return "商品已经取完";
}
// 用户拿到了这个商品,所以这个商品需要自减一
// 使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
// 由于用户已经取到了商品,所以redis中的数据也需要更新
redisUtil.set("goods",goods);
return "你已经成功获取商品";
} catch (Exception e) {
e.printStackTrace();
return "请重试";
} finally {
// 解锁
redisUtil.delete("lock");
}
}
}
}
@RestController
public class DemoController {
@Autowired
private RedisUtil redisUtil;
@RequestMapping("/demo")
public String demo(){
// 设置自旋超时时间
long timeoutAt = System.currentTimeMillis();
// 设置唯一标识
String uuid = UUID.randomUUID().toString();
// 自旋
while (true){
long now = System.currentTimeMillis();
// 5秒超时,退出
if (now - timeoutAt > 5000){
System.out.println("连接超时请重试");
return "连接超时请重试";
}
// 加锁,设置超时时间和唯一标识
boolean lock = redisUtil.setnx("lock", uuid,10, TimeUnit.SECONDS);
if (!lock){
continue;
}
try {
// 取goods的值
Integer goods = (Integer) redisUtil.get("goods");
if (goods <= 0){
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "商品已经取完");
return "商品已经取完";
}
// 用户拿到了这个商品,所以这个商品需要自减一
// 使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
// 由于用户已经取到了商品,所以redis中的数据也需要更新
redisUtil.set("goods",goods);
return "你已经成功获取商品";
} catch (Exception e) {
e.printStackTrace();
return "请重试";
} finally {
if (uuid.equals(redisUtil.get("lock"))){
// 解锁
redisUtil.delete("lock");
}
}
}
}
}
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.20.0version>
dependency>
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redisson(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0).setPassword("123456");
// 创建RedissonClient对象
return Redisson.create(config);
}
}
@RestController
public class DemoController {
@Autowired
private RedisUtil redisUtil;
@Resource
private Redisson redisson;
@RequestMapping("/test")
public String Test(){
RLock lock = redisson.getLock("lock");
// 设置超时时间30秒
lock.lock(30,TimeUnit.SECONDS);
try {
// 取goods的值
Integer goods = (Integer) redisUtil.get("goods");
if (goods <= 0){
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "商品已经取完");
return "商品已经取完";
}
// 用户拿到了这个商品,所以这个商品需要自减一
// 使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
System.err.println(Thread.currentThread().getName() +
Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
// 由于用户已经取到了商品,所以redis中的数据也需要更新
redisUtil.set("goods",goods);
return "你已经成功获取商品";
} catch (Exception e) {
e.printStackTrace();
return "请重试";
}finally {
lock.unlock();
}
}
}