本次我们迭代了8个版本,由简单到复杂,由功能单一变功能强大,由单一性变为通用性。
V1.0 单机锁版本,先构建一个简单的单机锁,在此基础上逐步完善,直至形成一个分布式锁
改pom,写yaml,主启动,配置类等在此就不再赘述。直接开启业务类的编写。
初始版本,单机锁
@Service
@Slf4j
public class InventoryService {
private Lock lock = new ReentrantLock();
public String sale(){
String message = "";
lock.lock();
String Value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//判断库存是否足够
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//库存足够,扣减库存
if (inventoryNumber > 0 ) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
message = "成功卖出一件,剩余" + inventoryNumber ;
System.out.println(Value);
System.out.println(message);
}else {
message = "全部售罄!";
}
}finally {
lock.unlock();
}
return message + "\t" + "服务端口号为: " + PORT;
}
@RestController
@Slf4j
@Api(tags = "测试分布式锁")
public class InventoryController {
@Resource private InventoryService inventoryService;
@ApiOperation("扣减库存,卖一个")
@GetMapping("/inventory/sale")
public String sale(){
return inventoryService.sale();
}
}
一个单机锁的业务就完成了,在实际生产中,使用一个单机锁往往是不够的,会造成很多的问题,也限制了程序的扩展能力。
所以我们都是要使用分布式锁,那么使用分布式锁就需要考虑到线程安全的问题。
由此,我们再创建一个module,就直接先复制粘贴原来项目的pom,yaml,配置类,业务类吧,改动yaml中的端口为跟上一个不同就行。
此时我们在这个新的module中的InventoryService 类中添加一个synchronized或者lock/unlock锁。
V2.0 创建一个新的module,模拟分布式系统,启动nginx,使用nginx配置两个module,
@Service
@Slf4j
public class InventoryService {
@Value("${server.port}") private String PORT;
@Resource private StringRedisTemplate stringRedisTemplate;
private Lock lock = new ReentrantLock();
public String sale(){
String message = "";
String key = "inventory001";
lock.lock();
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get(key);
//判断库存是否足够
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//库存足够,扣减库存
if (inventoryNumber > 0 ) {
stringRedisTemplate.opsForValue().set(key,String.valueOf(--inventoryNumber));
message = "成功卖出一件,剩余" + inventoryNumber ;
System.out.println(message);
}else {
message = "全部售罄!";
}
}finally {
lock.unlock();
}
return message + "\t" + "服务端口号为: " + PORT;
}
}
在主启动类中启动两个module,进行测试。
在浏览器进行测试http://192.168.10.100:/inventory/sale,一个一个点击时,nginx会默认轮询,可能不会出现问题,那我们就需要放出猛狗Jmeter进行压力测试!
如上图,2000个线程进行测试,测试我们发现,即使是两个module都加了锁,但是也发生了重复扣减库存的行为!那么我们由此得知,在分布式系统下,synchronized和lock锁都失效了!
在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程。
所以我们正式引入我们这篇的重点——分布式锁!
能干吗?!
1.跨进程+跨服务
2.解决超卖
3.防止缓存击穿
V3.1 引入分布式锁setnx
public String sale(){
String message = "";
String key = "MyRedisKey";
String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, Value);
if (!absent) {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
sale();
}else {
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//判断库存是否足够
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//库存足够,扣减库存
if (inventoryNumber > 0 ) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
message = "成功卖出一件,剩余" + inventoryNumber ;
System.out.println(message);
}else {
message = "全部售罄!";
}
}finally {
//一定要记住删除key!
stringRedisTemplate.delete(key);
}
}
return message + "\t" + "服务端口号为: " + PORT;
}
V3.2 上个版本,使用if递归重试的方式,但是在高并发下,容易造成线程的虚假唤醒,造成stackOverFlowError,不太推荐。所以我们可以使用while自旋来替代if判断的递归。
//V3.2 使用while自旋来替代if判断的递归,避免栈溢出
public String sale(){
String message = "";
String key = "MyRedisKey";
String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, Value);
while (!absent) {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//判断库存是否足够
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//库存足够,扣减库存
if (inventoryNumber > 0 ) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
message = "成功卖出一件,剩余" + inventoryNumber ;
System.out.println(message);
}else {
message = "全部售罄!";
}
}finally {
//一定要记住删除key!
stringRedisTemplate.delete(key);
}
return message + "\t" + "服务端口号为: " + PORT;
}
V4.1 在3.2版本结束后,我们又要考虑到,万一程序在运行中突然宕机,最后的 stringRedisTemplate.delete(key);都没进行到,那么这不是造成了一个死锁?所以我们需要加入一个过期时间,哪怕程序宕机了,也不至于出现程序被锁住的尴尬情况
//V4.1 设置过期时间防止死锁
public String sale(){
String message = "";
String key = "MyRedisKey";
String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, Value);
while (!absent) {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//判断库存是否足够
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//库存足够,扣减库存
if (inventoryNumber > 0 ) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
message = "成功卖出一件,剩余" + inventoryNumber ;
System.out.println(message);
}else {
message = "全部售罄!";
}
}finally {
//一定要记住删除key!
stringRedisTemplate.delete(key);
}
return message + "\t" + "服务端口号为: " + PORT;
}
V4.2 可以由上述的4.1版本代码看到,setnx设置锁与设置锁的过期时间expire,不在一行上,那么在高并发情况下,如果某一次进行到这两行代码之间的代码,突然造成了宕机,还没进行到设置过期时间这行,不还是没设置过期时间吗,这就又造成了死锁。
在这种情况下,跟V3.2的代码没什么区别,所以我们要保证原子性,将他们合并成一行。
public String sale(){
String message = "";
String key = "MyRedisKey";
String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, Value,30L,TimeUnit.SECONDS);
while (!absent) {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//判断库存是否足够
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//库存足够,扣减库存
if (inventoryNumber > 0 ) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
message = "成功卖出一件,剩余" + inventoryNumber + Value;
System.out.println(message);
}else {
message = "全部售罄!";
}
}finally {
//一定要记住删除key!
stringRedisTemplate.delete(key);
}
return message + "\t" + "服务端口号为: " + PORT;
}
使用jmeter压测,
redis查询表示程序通过。
V5.0 防止误删key,只允许删除自己的key,不允许删除别人的key!例如A先抢到线程,A开始工作,过期时间为30秒,但是A到30秒还没干完,没干完就被删了!此时B发现redis数据库中key已经空了,抢占到线程,开始工作,但是此时A还在执行代码,2秒以后,A执行完了,在finally中删除key,那么此时的key是B的key啊!不是A的key,造成了误删。
public String sale(){
String message = "";
String key = "MyRedisLockKey";
String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key,Value,30L,TimeUnit.SECONDS);
while (!absent) {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//判断库存是否足够
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//库存足够,扣减库存
if (inventoryNumber > 0 ) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
message = "成功卖出一件,剩余" + inventoryNumber + "\t" + Value;
System.out.println(Thread.currentThread().getId());
System.out.println(message);
}else {
message = "全部售罄!";
}
}finally {
// v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(Value)) {
stringRedisTemplate.delete(key);
}
}
return message + "\t" + "服务端口号为: " + PORT;
}
V6.0 在4.2版本中,我们讨论了设置过期时间与加锁在同一行的问题,这样保证了程序的原子性,保证了程序宕机时,不会出现问题。
所以在5.0版本时,我们可以发现,是不是同一把锁的判断与删除操作也不是同一行(不是同一个命令),这就有可能在高并发下造成错误。所以我们要保证程序的原子性。这时我们就需要使用lua来保证原子性。
关于lua脚本,请搜索lua教程。这里不做太多的讨论。
// V6.0 删除key操作,使用lua脚本删除,保证其原子性,在高并发下代码性能更好
public String sale(){
String returnMessage = "";
String key = "MyRedisLockKey";
String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//获取库存数
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//库存足够,扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
returnMessage = "库存扣减成功,剩余库存:" + inventoryNumber + "\t" + value;
System.out.println(returnMessage);
}else {
returnMessage = "全部售罄!";
}
}finally {
// if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(value)) {
// stringRedisTemplate.delete(key);
// }
//V6.0 将判断+删除自己的合并为lua脚本保证原子性
String luaScript = "if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript,Boolean.class), Arrays.asList(key),value);
}
return returnMessage + "\t" + PORT;
}
V7.0 到这一步,我们已经清楚的知道,我们还需要一个可重入的锁,允许同一个线程多次获取同一把锁。那么reentrant或者synchronized就实现了可重入性呀!
但是!我们之前已经讲过,reentrant或者synchronized只能在单个JVM实例下有用,在分布式系统下就失效了,而我们现在就是个分布式系统。
所以我们需要自己手写实现在分布式下的可重入锁。
那么思考一下,redis中什么类型的数据结构可以满足实现可重入锁呢?
那就是HSET,可以实现加锁的次数,解锁时又可以将加了锁的数值减1。如果为0,那就是没有锁了。
小总结:setnx可以解决有无分布式锁的问题,hset不但解决了分布式锁的问题,还实现了可重入的问题。
介绍完毕。现在我们开始着手V7.1的编写。
要实现以上的功能,就要编写lua脚本,用lua脚本加锁解锁,以保证原子性。
lua脚本加锁的逻辑:
加锁的lua脚本:
hincrby可以替代hset命令,所以合并一下可得:
lua脚本解锁的逻辑:
解锁的lua脚本:
上述前提工作完成后,我们就可以编写一个自己的Lock,实现可重入的分布式锁了。
我们要了解的是,我们这个锁也是对Lock锁的接口规范定义来进行代码的落地。
V7.0 通过实现JUC里面的Lock接口,实现redis分布式锁RedisDistributedLock
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName)
{
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
this.expireTime = 30L;
}
@Override
public void lock()
{
tryLock();
}
@Override
public boolean tryLock()
{
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
if(time != -1L){
this.expireTime = unit.toSeconds(time);
}
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("script: "+script);
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(50);
}
return true;
}
/**
*干活的,实现解锁功能
*/
@Override
public void unlock()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
" return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
// nil = false 1 = true 0 = false
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
if(flag == null)
{
throw new RuntimeException("This lock doesn't EXIST");
}
}
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
InventoryService 类中原来使用setnx来加锁的代码块就可以替换成我们自己的Lock了。
@Service
@Slf4j
public class InventoryService {
@Value("${server.port}") private String PORT;
@Resource private StringRedisTemplate stringRedisTemplate;
private Lock myRedisDistributedLock = new MyRedisDistributedLock(stringRedisTemplate,"MyRedisLock");
// V7.0 lua脚本结合hset考虑锁的可重入性
public String sale(){
String returnMessage = "";
myRedisDistributedLock.lock();
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//获取库存数
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//库存足够,扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
returnMessage = "库存扣减成功,剩余库存:" + inventoryNumber + "\t";
System.out.println(returnMessage);
}else {
returnMessage = "全部售罄!";
}
}finally {
myRedisDistributedLock.unlock();
}
return returnMessage + "\t" + PORT;
}
但其实到这里还是有点小问题的,就是这个我们自己写的Lock已经被写死了,我不具备扩展性和通用性,只能供给Redis一个使用。当zookeeper或者mysql需要分布式锁,那么就需要再加,所以为了扩展性和通用性,我们引入工厂模式来完善。
V7.1 引入工厂模式完善代码,提升扩展性和通用性
DistributedLockFactory:
@Component
public class DistributedLockFactory
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
public Lock getDistributedLock(String lockType)
{
if(lockType == null) return null;
if(lockType.equalsIgnoreCase("REDIS")){
lockName = "zzyyRedisLock";
return new RedisDistributedLock(stringRedisTemplate,lockName);
} else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
//TODO zookeeper版本的分布式锁实现
return new ZookeeperDistributedLock();
} else if(lockType.equalsIgnoreCase("MYSQL")){
//TODO mysql版本的分布式锁实现
return null;
}
return null;
}
}
RedisDistributedLock :
//@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName){
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
this.expireTime = 30L;
}
@Override
public void lock(){
tryLock();
}
@Override
public boolean tryLock(){
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
/**
* 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
if(time != -1L){
this.expireTime = unit.toSeconds(time);
}
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("script: "+script);
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(50);
}
return true;
}
/**
*干活的,实现解锁功能
*/
@Override
public void unlock()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
" return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
// nil = false 1 = true 0 = false
System.out.println("lockName: "+lockName);
System.out.println("uuidValue: "+uuidValue);
System.out.println("expireTime: "+expireTime);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
if(flag == null)
{
throw new RuntimeException("This lock doesn't EXIST");
}
}
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
//===下面的redis分布式锁暂时用不到=======================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale()
{
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0)
{
inventoryNumber = inventoryNumber - 1;
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t服务端口:" +port;
System.out.println(retMessage);
return retMessage;
}
retMessage = "商品卖完了,o(╥﹏╥)o"+"\t服务端口:" +port;
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock();
}
return retMessage;
}
}
V7.1 改造完成,进行测试。
V7.1版本下,我们仅仅是写了一个我们自己的分布式锁,RedisDistributedLock 以及引入了RedisDistributedFactory工厂模式,并没有测试可重入性。所以我们在V7.1下测试可重入性。
在inventoryService类中,新增一个方法,此方法里面加锁解锁,看看进入该方法的是不是同一个。
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale()
{
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
System.out.println(retMessage);
//新增的方法,测试进出该方法的是不是同一把锁
testReEnter();
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
private void testReEnter()
{
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
System.out.println("################测试可重入锁#######");
}finally {
redisLock.unlock();
}
}
}
public class MyRedisDistributedLock implements Lock {
public MyRedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
this.expireTime = 50L;
}
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
private long expireTime;
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {
tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L) {
//打印加锁的uuid,与解锁的uuid进行对比,查看另一个加了锁又解锁的的方法的锁与原本的锁是否是同一个,因为是同一个才能保证重入性
System.out.println("lock():lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
String script =
"if redis.call('exists',KEYS[1])==0 or redis.call('hexists',KEYS[1],ARGV[1])==1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(60);
}
return true;
}
return false;
}
@Override
public void unlock() {
//打印加锁的uuid,与解锁的uuid进行对比,查看另一个加了锁又解锁的的方法的锁与原本的锁是否是同一个,因为是同一个才能保证重入性
System.out.println("unlock():lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
String script = "if redis.call('hexists',KEYS[1],ARGV[1])==0 then " +
"return nil " +
"elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)==0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList(lockName), uuidValue);
if (flag == null) {
throw new RuntimeException("this lock doesn't exists!!");
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
测试结果:不是同一个锁,可重入性还没实现。
ThreadId一致,但是uuid不一致,为什么?
引出V7.2版本
DistributedLockFactory:
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
private long expireTime;
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue)
{
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
//注意:这里原本的IdUtil.simpleUUID()就改为从工厂传来的uuidValue了
this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock()
{
this.tryLock();
}
@Override
public boolean tryLock()
{
try
{
return this.tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
if(time != -1L)
{
expireTime = unit.toSeconds(time);
}
System.out.println("lock():lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
{
try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
}
return true;
}
@Override
public void unlock()
{
System.out.println("unlock():lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
if(flag == null)
{
throw new RuntimeException("没有这个锁,HEXISTS查询无");
}
}
//=========================================================
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale()
{
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
this.testReEnter();
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号:"+port;
}
private void testReEnter()
{
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try
{
System.out.println("################测试可重入锁####################################");
}finally {
redisLock.unlock();
}
}
}
使用jmeter进行测试,单机+并发+可重入性都通过。如图所示:
到这里,我们还需考虑业务执行时间的问题,尽管我们所设置的过期时间已经足够业务执行完毕,但是在使用中如果碰到拥堵,一个线程执行业务的时间远远大于线程的过期时间了,怎么办?
V8.0 新增自动续期功能
那么可以设置一个很大的过期时间吗?不行,考虑到系统的健壮性以及扩展性,还是不要写死这个时间,那么我们就需要引入V8.0版本——自动续期。
使用lua脚本
注意:在java代码块中编写lua脚本,lua脚本的字符串换行时,要在每行最后一个字符后面加空格,不然会报错误。很多初学者可能会忽略这个细节,造成错误。
public class MyRedisDistributedLock implements Lock {
public MyRedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuidValue + ":" + Thread.currentThread().getId();
this.expireTime = 50L;
}
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuidValue;
private long expireTime;
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {
tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time == -1L) {
System.out.println("lock():lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
String script =
"if redis.call('exists',KEYS[1])==0 or redis.call('hexists',KEYS[1],ARGV[1])==1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
TimeUnit.MILLISECONDS.sleep(60);
}
//自动加锁功能
this.renewExpire();
return true;
}
return false;
}
@Override
public void unlock() {
System.out.println("unlock():lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
String script = "if redis.call('hexists',KEYS[1],ARGV[1])==0 then " +
"return nil " +
"elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)==0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList(lockName), uuidValue);
if (flag == null) {
throw new RuntimeException("this lock doesn't exists!!");
}
}
//自动加锁,new Timer在JUC中有涉及,只一个java 的定时器
private void renewExpire()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
new Timer().schedule(new TimerTask()
{
@Override
public void run()
{
if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
renewExpire();
}
}
},(this.expireTime * 1000)/3);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
在MyRedisDistributedLock类中的添加了一个自动续期的方法,这个方法中有一个new Timer,在JUC中有涉及,感兴趣的可以看看JUC。
new timer是一个java的定时器任务,schedule方法可以添加计时器任务,以及多久的延迟。
完成上述自动续期的功能后,我们去测试,此时在InventoryService中人为的sleep一段时间,看看是否会触发自动续期。
到这里,8个版本就完成了,redis分布式锁的整体功能都已经完善了,其中细节还需补充,例如此时的锁是一个单机锁,如果碰到了严重的单点故障问题,导致这个单机的redis分布式锁挂了怎么办?
你可能想到,创建锁集群,这个挂了其他的补上!但通常这是不行的。原因如图所示:
那么如何解决呢?请看另一篇《Redlock算法和底层源码分析》