前言
当我们有了前一篇的经验以后继续说说我们写锁的时候应该注意的细节
1独占排他锁
2防止死锁
3防止误删
4原子性
5可重入
6自动续期
当然我们还得了解一下redis的数据类型Hset
提示:以下是本篇文章正文内容,下面案例可供参考
一、Hset?
1redis的一种数据结构其结构为
可以看到他的数据结构类似于Map<String,Map<String,String>>这种,为什么我们选择这种呢,分布式锁的应用即为高并发,分布式的情况下进行使用,比如我们同一时刻只有被抢到锁的处理并减库存,我们给他加一个key为lock,value为uid的锁,这时我们知道了这个唯一商品现在是被谁秒杀到了进行程序的处理,当然现实我们不可能这么简单要考虑缓存,并发数量,消峰,等等用到消息列队,这我们只做个示例,好了现在知道比如这个叫lock锁的被这个uid抢到了,在除了抢物品的同时我们还要做减库存操作,这是加入调用其他方法我们还要能够重入锁保证串行执行那么我们的hset就能够使用了 lock->uid,1,初始重入次数为1 直到扣减为0
二、加锁流程
//是否存在锁,不存在直接获取锁(抢到锁),如果存在看看是不是当前线程uid的锁,是的话value+1重入,否则获取锁失败,流程很简单这边我的uid用uuid实现大家也可以雪花算法或者其他方式实现,只要保证分布式下的uid唯一就行,我们建立一个类似工厂结构的类需要什么锁就获取什么所
@Component
public class LockClient {
@Autowired
private StringRedisTemplate redisTemplate;
public RedisRLock getRedisLock(String lockName,String uuid){
return new RedisRLock(lockName,uuid,redisTemplate);
}
public RedisRLock getZkLock(String lockName,String uuid){
return new getZkLock(lockName);
}
}
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class RedisRLock implements Lock {
private String lockName;
private StringRedisTemplate redisTemplate;
private String uuid;
private long expire = 30L;
RedisRLock(){
}
RedisRLock(String lockName,String uuid, StringRedisTemplate redisTemplate){
this.lockName =lockName;
this.uuid = uuid;
this.redisTemplate = redisTemplate;
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
return this.tryLock(expire, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
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 (!this.redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuid,String.valueOf(time))){
Thread.sleep(50);
}
expireLock();
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";
Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(lockName),uuid);
if (flag == null){
throw new IllegalMonitorStateException();
}
}
//续期每隔一段时间访问一下 进行续期
private void expireLock(){
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(redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class),Arrays.asList(lockName),uuid,String.valueOf(expire))){
expireLock();
}
}
},expire*1000/3);
}
@Override
public Condition newCondition() {
return null;
}
}
RedisRLock implements Lock 我们实现锁的加锁和解锁方法
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 (!this.redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuid,String.valueOf(time))){
Thread.sleep(50);
}
expireLock();
return true;
我们先读一下这lua脚本判断如果锁不存在我们可以加锁或者呢这个锁存在了我们要进行重入所以这里就用了或成立我们就调用
redis.call(‘hincrby’,KEYS[1],ARGV[1],1)
这个的意思就是自增如果没锁就是 lock uuid 1如果再次重入比如我第二次同一个线程获取锁那么就会自增1变成 lock uuid 2
这里的keys[1] Arrays.asList(lockName) 锁的名称
ARGV我传了2个 一个是uuid 标记某个线程 另一个是 超时时间 现在我们的加锁满足了1独占排他锁,可重入,防止没有过期时间引发的死锁和加锁并设置过期时间的原子性
1加锁成功走业务流程和expireLock();这里的expireLock();我们下面再讲
2如果失败我们sleep让出过一会在继续尝试
三、解锁流程
@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";
Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(lockName),uuid);
if (flag == null){
throw new IllegalMonitorStateException();
}
}
这个也很好理解
1看看这个要解锁的线程锁还在不 如果不在的话我们和ReentrantLock一样抛出异常
2如果在的话还是自己线程的锁我们重入次数减一后判断是否为0 如果为0我们就删除锁 说明重入锁全部解完
3否则我们return 0 不报错表示已经解了一次该做啥做啥
这里我们同样做到防误删,原子性等,当然在集群当中我们主io还没有写入从redis就挂了这种问题我会单独对redis的红锁以及后面的redission提出一篇文章
四、自动续期
当然使我们老生常谈的看门狗机制了,就是找一个线程我们专门去处理过期时间,建议线程池,有些
业务因为种种原因我们的过期时间都结束了,代码块还没有执行完,会导致,其他线程来立刻获取到锁,虽然我们解决了误删的问题但是这种锁自动过期的问题也难以忍受
private void expireLock(){
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(redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class),Arrays.asList(lockName),uuid,String.valueOf(expire))){
expireLock();
}
}
},expire*1000/3);
}
1每隔锁的2/3过期时间看看锁是不是存在,存在说明这个还在使用但是业务没有处理完,起来继续重置过期时间,然后再把这个方法递归调用一下,直到下次调用时,发现锁不存在了 那好吧说明锁都没了我们就不递归调用了等待线程cg
我们举个栗子测试一下 设置一个key为stock的10000件商品
java开了2个服务通过负载均衡打过来做压力测试
@SneakyThrows
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
@GetMapping("/lock")
private void lockTest(){
String uuid = UUID.randomUUID().toString();
RedisRLock redisRLock = lockClient.getRedisLock("lock",uuid);
redisRLock.lock();
try {
String stock = stringRedisTemplate.opsForValue().get("stock");
if (!StringUtils.isEmpty(stock)&&Integer.valueOf(stock)>0){
Integer st =new Integer(stock);
stringRedisTemplate.opsForValue().set("stock", String.valueOf(--st));
}
test(redisRLock);
}
finally {
redisRLock.unlock();
}
}
@SneakyThrows
public void test(RedisRLock redisRLock){
redisRLock.lock();
System.out.println(Thread.currentThread().getId());
redisRLock.unlock();
}
总结
本文仅仅简单介绍了redis基于lua的使用,我们实现了分布式锁并且在下不同的服务情况下依旧能实现能实现商品的不超卖