一、什么是分布式锁?
分布式锁是相对于单体单机应用而言的一种锁机制。在单机应用时由于共享一个jvm,可以使用同一个java Lock对象进行获取锁,解锁操作。当为分布式集群时存在跨机器请求执行,无法共享同一个java对象锁,但又需要对需要加锁保护的代码逻辑进行执行,此时分布式锁就相应而出现了。
百度百科这这样介绍到:分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。
如上图:同一商品在同一时间有多个用户进行下单操作,在负载均衡服务器的地址路由之后可能会将请求分发到不同的节点上面。不同的节点进行出库操作。如果在没有并发的情况下,某一条节点接收订单请求之后,执行完出库操作,以及数据库将数据进行同步之完成之后,下一个订单请求再进来判断库存执行出库都毫无问题。但是如果当请求并发比较高时,在其中一个节点在操作数据库时,另一个节点也执行到了操作数据库过程,那么有可能虽然都下订单成功,实际上下订单数量已经大于实际库存数量,将导致下单问题出现。
那么,分布式锁是如果解决上述问题的,实际上分布式锁主要依靠所有集群节点操作一个共享数据实现锁逻辑。比如通过读写同一个数据库,读写同一个redis等等来实现在一个时间内只有获得锁的请求可以去操作库存数据库,等数据同步完成之后,在进行解锁操作。如下图:如果共享存储中存在lock_name 时,其他请求等待。当获取锁的请求执行完成之后,则删除lock_name。其他某一条线程在获得锁再在共享存储中设置上lock_name。从而达到同一时间只有一条请求执行加锁逻辑代码块。
二、使用Redis的手写实现分布式锁
在使用Redis手写实现主要是借助于setnx(set if not exists) 命令特性(相同key值只有一个执行命令可以返回正确,其他失败),客户端效果如下:
借此特性,我们在并发情况下可以根据此特性保证只有一个请求获得锁(返回1的)。当该请求需要执行加锁逻辑时,首先去执行redis setnx 方法,获得成功返回后,执行加锁逻辑代码。此时其他并发请求在执行到加锁逻辑时,执行setnx 则返回失败,此时可以不间断(间隔时间越短效率越高,但是耗费资源)去执行setnx操作。直到当前获得锁的请求执行完加锁逻辑后,执行删除key操作进行解锁后,其他线程才可以获得锁。setnx 我们在java代码中可以通过如下函数实现:
redisTemplate.opsForValue().setIfAbsent(LOCK_NAME, "1")
首先我们定义一个库存对象Stock.java,包含库存当前数量以及下订单操作。在实际中可以是多台集群机器中的sql执行减一修改操作。
package com.xiaohui.bean;
public class Stock {
//库存当前数量
public static int count = 1;
/**
* 下订单,减少库存操作
* @return true 下单成功,false 下单失败
*/
public static boolean reduceStock(){
if(count <= 0){
return false;
}
try {
Thread.sleep(2000);
}catch (Exception e){e.printStackTrace();}
count-- ;
return true;
}
}
接下来我们执行一段没有加锁的测试代码 LockMain.java
package com.xiaohui.web;
import com.xiaohui.bean.Stock;
public class LockMain {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
boolean b = Stock.reduceStock();
System.out.println(Thread.currentThread().getName()+"下单:"+( b ? "成功":"失败"));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
boolean b = Stock.reduceStock();
System.out.println(Thread.currentThread().getName()+"下单:"+( b ? "成功":"失败"));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
boolean b = Stock.reduceStock();
System.out.println(Thread.currentThread().getName()+"下单:"+( b ? "成功":"失败"));
}
}).start();
try {
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("Stock.count = " +Stock.count);
}
}
打印如下:我们可以看到在没有锁的情况下我们将库存更新为了负数。这不是我们预期的小于0时下订单失败的效果的。
接下来我们使用redisTemplate对象通过操作redis 实现分布式锁。
代码环境使用SpringBoot 工程 + Redis +web工程测试
1,pom.xml
4.0.0
com.xiaohui
zklockdemo
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.1.7.RELEASE
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
2.7.3
org.springframework.boot
spring-boot-starter-web
2, application.properties 主要配置web模块访问端口以及redis的链接信息
server.port=8888
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# 连接超时时间(毫秒)
spring.redis.timeout=5000
# 连接池设置
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-active=8
3,Redis 配置类RedisConfig.java
package com.xiaohui.cfg;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 值采用json序列化
template.setValueSerializer(jacksonSeial);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
// 设置hash key 和value序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jacksonSeial);
template.afterPropertiesSet();
return template;
}
}
4.Redis锁实现类(重要)ReadisLock.java
package com.xiaohui.bean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
@Component
public class ReadisLock implements Lock {
@Autowired
RedisTemplate redisTemplate;
private static String LOCK_NAME = "lock_name";
@Override
public void lock() {
while (true){
Boolean name = redisTemplate.opsForValue().setIfAbsent(LOCK_NAME, "222");
//为防止加锁后报错无法解锁,可以给缓存设置失效时间,解决死锁问题。
// Boolean name = redisTemplate.opsForValue().setIfAbsent(LOCK_NAME, "1",15,TimeUnit.SECONDS);
if(name){ // true 未加锁 加锁成功
System.out.println(Thread.currentThread().getName()+" 加锁成功。。");
break;
}else{// false 已加锁
System.out.println(Thread.currentThread().getName()+"继续等待....");
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
redisTemplate.delete(LOCK_NAME);
System.out.println(Thread.currentThread().getName()+" 完成解锁。。");
}
@Override
public Condition newCondition() {
return null;
}
}
再此类中我们实现了java包中的Lock接口。主要实现其 lock加锁方法,以及unlock解锁方法。
在lock方法中我们通过循环redisTemplate.opsForValue().setIfAbsent(LOCK_NAME, "222") 对redis进行操作,如果设置成功则表示取得锁。退出lock函数。其他没有获取成功的将继续执行等待获取。。。
在解锁函数中主要就是删除其锁过程。
注意:为了避免在取得锁后,在解锁之前报错,导致无法解锁出现死锁状态,我们可以通过setIfAbsent(LOCK_NAME, "1",15,TimeUnit.SECONDS);四个参数的方法为缓存设置失效时间,来保证在无法解锁的情况下 过期锁自动解开。
5,web测试代码
package com.xiaohui.web;
import com.xiaohui.bean.ReadisLock;
import com.xiaohui.bean.Stock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WebController {
@Autowired
ReadisLock lock;
@GetMapping("/startReduce")
public String startReduce(){
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
boolean b = Stock.reduceStock();
System.out.println(Thread.currentThread().getName()+"下单:"+( b ? "成功":"失败"));
lock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
boolean b = Stock.reduceStock();
System.out.println(Thread.currentThread().getName()+"下单:"+( b ? "成功":"失败"));
lock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
boolean b = Stock.reduceStock();
System.out.println(Thread.currentThread().getName()+"下单:"+( b ? "成功":"失败"));
lock.unlock();
}
}).start();
try {
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("Stock.count = " +Stock.count);
return "set ok!";
}
}
在该接口中我们模拟了三个并发操作下订单逻辑。并在每一个下订单前后都加上了锁操作。
6,SpringBoot启动类代码
package com.xiaohui;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}
7,启动访问web 测试地址 http://127.0.0.1:8888/startReduce 控制台打印如下:我们可以看到 第一个加锁成功的线程13 下单成功,后面在渠道锁的都下单失败,和预期下单锁效果一致。
Thread-13 加锁成功。。
Thread-15继续等待....
Thread-14继续等待....
Thread-14继续等待....
//若干重复打印14 15 线程等待
Thread-14继续等待....
Thread-13下单:成功
Thread-15继续等待....
Thread-13 完成解锁。。
Thread-15 加锁成功。。
Thread-15下单:失败
Thread-14继续等待....
Thread-15 完成解锁。。
Thread-14 加锁成功。。
Thread-14下单:失败
Thread-14 完成解锁。。
项目工程结构如下: