使用原生Redis完成分布式锁
假设我们需要对redis中对商品库存进行减少,但是redis中可能会不存在此商品信息,此时我们就需要从数据库中取出库存将其放入redis。我们要对这个操作进行添加分布式锁。
首先,先理清业务的流程:
- 检查redis中是否存有商品数据,如果没有就开始获取锁。
- 自旋来获取锁,获取到锁之后判断是否已经有人完成添加此商品到redis的操作如果已经完成就退出,否则就进行获取数据并添加到redis中。
- 释放锁。
整个业务最重要的就是如何获取锁和释放锁,要保证整个过程不会出现任何的并发问题(两个线程拿到同一个商品的锁之类的)。
redis实现分布式锁和synchronized的操作相似
问题
如果有线程获取到锁,但是在执行业务的时候报错了这个锁就不会被释放怎么办?
解决方法:在向redis中添加数据的时候,给数据添加一个时间,时间一到立马删除,但是这两个操作必须是一起执行的,所以需要使用lua脚本来保证原子性。
在释放锁的过程中释放了别的其他线程的锁?
线程1获取到锁之后,在执行业务的时候时间太长,导致redis自动消除了数据完成了锁的释放,然后线程2获取到锁开始执行业务,这时线程1执行完成开始释放锁,这个时候会导致线程1释放了线程2的锁,然后线程2释放线程3······。
解决方法:在添加数据的时候给这个数据添上自己的标记,在释放的时候检查标记是否当前线程的标记,因为在删除数据的时候需要先检查数据有没有被打上标记,所以需要使用lua脚本来保证操作的原子性。
如何保证一定会执行释放锁的操作?
使用try-finally,把释放的操作放在finally代码块中。
获取锁
先查看redis中是否存有对应的商品ID,如果没有对应的id,就可以使用redis的set命令对redis中添加数据,添加数据的键为对应商品的ID,值为获取的雪花ID。生成成功就放回雪花ID。
删除锁
先获取redis中商品id对应的值,如果值和拥有的值一样就可以进行删除操作(使用lua保证原子性)。
分布锁类
package com.example.demo.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
public class RedisLock {
private RedisTemplate<String,Object> redisTemplate;
private final String CHECK_LOCK=" local lock=redis.call('get',KEYS[1]) " +
"if lock~=false " +
"then " +
"return false " +
"else " +
"redis.call('set',KEYS[1],ARGV[1],'EX',ARGV[2]) " +
"end " +
"return true ";
private final String DEL_LOCK=" local lock=redis.call('get',KEYS[1]) " +
"if lock~=ARGV[1] " +
"then " +
"return false " +
"else " +
"redis.call('del',KEYS[1]) " +
"end " +
"return true";
public RedisLock(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public String getLock(String name, Long time, Long timeout){
Long startTime=System.currentTimeMillis();
String token = null;
do {
if((System.currentTimeMillis()-startTime)>timeout){
break;
}else{
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace(System.out);
return null;
}
}
token=tryGetToken(name,time);
}while (token==null);
return token;
}
private String tryGetToken(String name,Long time){
String id= String.valueOf(SnowFlake.ToGetAll());
RedisScript<Boolean> redisScript=new DefaultRedisScript<>(CHECK_LOCK,Boolean.class);
if(Boolean.TRUE.equals(redisTemplate.execute(redisScript, Collections.singletonList(name), id, time))){
return id;
}else{
return null;
}
}
public void delLock(String name,String token){
RedisScript<Boolean> redisScript=new DefaultRedisScript<>(DEL_LOCK,Boolean.class);
redisTemplate.execute(redisScript, Collections.singletonList(name),token);
}
}
Integer stock= (Integer) redisTemplate.opsForHash().get("dishes",String.valueOf(shopping.getDishesId()));
if(stock==null) {
//开始获取redis锁
RedisLock redisLock = new RedisLock(redisTemplate);
String token = null;
try {
do {
token = redisLock.getLock(String.valueOf(shopping.getDishesId()), (long) (20000), (long) (2100));
//检查redis中是否存有数据(是否有线程完成了此操作)
stock= (Integer) redisTemplate.opsForHash().get("dishes",String.valueOf(shopping.getDishesId()));
if(stock!=null){
break;
}
if(token!=null){
//再次判断
stock= (Integer) redisTemplate.opsForHash().get("dishes",String.valueOf(shopping.getDishesId()));
if(stock!=null){
break;
}
System.out.println("获取到锁并且开始运行业务");
Integer dishesStock = dishesMapper.selectStocksById(shopping.getDishesId());
redisTemplate.opsForHash().put("dishes",String.valueOf(shopping.getDishesId()),dishesStock);
}
} while (token == null);
} finally {
//释放锁
redisLock.delLock(String.valueOf(shopping.getDishesId()), token);
}
}