死锁

错误例子

解决方式

 防止死锁 通过设置超时时间
 不要使用setnx key   expire 20  不能保证原子性 如果setnx程序就挂了 没有执行expire就死锁了
 reidis2.8版本提供 set lock:key1 true ex 5 nx 方式 保证了  setnx+expire原子性方式执行(秒为单位)

锁超时

错误例子

        String lockKey="stock:product:1";
boolean isGetLock=false;
try{
//假设是原子性的 获取锁并设置锁10秒
isGetLock==setnx(lockKey,10);
if(!isGetLock){
throw new Exception("系统繁忙!请稍后再试");
}
//模拟需要执行12秒
Thread.sleep(12);
}finally {
if(isGetLock){
del(lockKey);
}
}

假设有线程A线程B 2个线程

线程A率先拿到锁因为我们设置的锁10秒自动释放(redis过期时间10秒) 而我们程序需要执行10秒以上

10.1ms秒的时候线程B进来 因为redis锁key已经过期成功拿到锁 并阻塞在12秒处

12秒后线程A 执行完 执行del操作 导致释放了线程B的锁

解决方式1

 String lockKey="stock:product:1";
boolean isGetLock=false;
//用来标识当前身份
String currentIndex=UUID.randomUUID().toString();
try{
//假设是原子性的 获取锁并设置锁10秒 同时设置一个值为currentIndex
isGetLock==setnx(lockKey,currentIndex,10);
if(!isGetLock){
throw new Exception("系统繁忙!请稍后再试");
}
//模拟需要执行12秒
Thread.sleep(12);
}finally {
if(isGetLock){
String lockValue=get(lockKey);
//表示是当前线程的锁 释放
if(lockValue!=null&&lockValue.equals(currentIndex)) {
del(lockKey);
}
}
}

方式1优化方案

简单一看 好像并没有什么问题 但是需要注意 get 比较 和del并不是原子性的

比如 线程A get完之后 lockkey因为超时释放   线程B 成功获得锁    线程A再执行if判断 会删除调线程B的锁

改为lua脚本

if redis.call("get",KEYS[]==ARGV[])  then
return redis.call("del","KEYS1")
else
return
end

主从切换

线程A从主节点加锁成功  这个时候主节点挂掉,从节点替换主节点 锁数据并没有同步过来 导致2个线程会获得锁  只会在 挂掉时 从节点还未同步时导致这样的情况 极少情况发生 不过一般业务场景都能接受
 

可重入锁实现

/**
* @Auther: liqiang
* @Date: 2019/7/14 14:59
* @Description:
*/
public class RedisWithReentrantLock {
private ThreadLocal<Map<String,Integer>> lockers=new ThreadLocal<>();
private Jedis jedis;
public RedisWithReentrantLock(Jedis jedis){
this.jedis=jedis;
}
/**
* 加锁
*/
private boolean _lock(String key){
String value=String.valueOf(System.currentTimeMillis());;
return jedis.set(key,value,"nx","ex",5L)!=null;
}
/**
* 释放锁
* @param key
*/
private void _unlock(String key){
jedis.del(key);
} /**
* 从线程缓存获取map 没有就初始化一个
* @return
*/
private Map<String,Integer> currentLockers(){
Map<String,Integer> refs=lockers.get();
if(refs==null){
refs=new HashMap<String,Integer>();
lockers.set(refs);
}
return lockers.get();
} /**
* 可重入锁
* @param key
* @return
*/
public boolean lock(String key){
/**
* 选择map的原因是 一个线程里面可能有很多加锁的地方
*/
Map<String,Integer> lockers=currentLockers();
/**
*如果存在 表示是重入加锁
*/
if(lockers.containsKey(key)){
lockers.put(key,lockers.get(key)+1);
//延长过期时间
jedis.expire(key,5000);
return true;
}
//走到这里表示是头部第一次加锁 加锁并对应map数量+1
boolean isGetLock=_lock(key);
lockers.put(key,1);
return isGetLock;
} /**
* 释放锁
* @param key
* @return
*/
public boolean unLock(String key){
/**
* 获得map
*/
Map<String,Integer> lockers=currentLockers();
/**
* 表示key未加过锁 或者释放了
*/
Integer refCnt=lockers.get(key);
if(refCnt==null){
return false;
}
//-1
refCnt-=1;
//大于0表示不是头部锁释放
if(refCnt>0){
lockers.put(key,refCnt);
}else{
//小于等于0 表示是头部锁释放 删除mapkey
lockers.remove(key);
/**
* 释放锁
*/
_unlock(key);
}
return true;
}
public static void main(String[] args) {
Jedis conn = new Jedis("127.0.0.1",6379);
conn.select(1);
RedisWithReentrantLock redisWithReentrantLock=new RedisWithReentrantLock(conn);
String lockKey="lock:key3";
redisWithReentrantLock.lock(lockKey);
redisWithReentrantLock.lock(lockKey); redisWithReentrantLock.unLock(lockKey);
redisWithReentrantLock.unLock(lockKey);
}
}

一些建议

建议涉及并发的地方能用原子性操作就用原子性

例子一

       tock stock=stockDao.get(id);
if(stock.getNumber()-10<0){
throw new Exception("库存不足");
}
stock.setNumber(stock.getNumber-10);
stockDao.update(stock);

这种情况就算加锁的情况 如果出现上面说的几种极端情况 或者锁失效了 会导致超卖以及库存异常问题

优化方案

         Stock stock=stockDao.get(id);
/**
* 这里可能会疑惑 下面有原子性的update加 where校验超卖 这一步是否不需要了
* 个人理解 程序进行校验 总比全部堆到数据库校验好的多
* 比如库存卖完了 还持续有并发请求 在这里就可以全部挡在外面
*/
if(stock.getNumber()-10<0){
throw new Exception("库存不足");
}
stock.setNumber(stock.getNumber-10);
//原子性的update
Integer updateNumber=stockDao.excuteSql("update stock set number-=10 where id=:id and number>=0",id);
//表示未能成功修改
if(updateNumber<=0){
throw new Exception("库存不足");
}

redis则使用对应redis递增递减

对于提供给管理员的库存盘点 也是使用原子性递增递减

盘增

比如当前库存是10  管理员调整20  则是+10  而不要直接set 20   不然并发时 10 卖了5  这个时候20才提交 则变成了20  如果+10 则变成15

盘减

比如当前库存是10 管理员 需要调整为5    并发时减成了0  执行update stock set number-=5 where id=:id and number>=0   number>=0并不成立所以修改失败

高并发时建议(比如秒杀场景)

将库存全量到redis  通过Incrby 命令实现原子性递增递减   如果消息发送失败需要进行补偿

update stock set number-=10 where id=:id and number>=0 通过mq 队列异步执行 否则会出现同一个库存并发改 部分是失败数据库抛出waitLock  tps就上不去 还会有大量请求到数据库 可能把redis

弄挂

 

redis实现分布式锁需要考虑的因素以及可重入锁实现的更多相关文章

  1. Redisson 分布式锁源码 01:可重入锁加锁

    前言 相信小伙伴都是使用分布式服务,那一定绕不开分布式服务中数据并发更新问题! 单系统很容易想到 Java 的各种锁,像 synchronize.ReentrantLock 等等等,那分布式系统如何处 ...

  2. redis分布式锁-可重入锁

    redis分布式锁-可重入锁 上篇redis实现的分布式锁,有一个问题,它不可重入. 所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞. 同一个 ...

  3. Java并发编程:自己动手写一把可重入锁

    关于线程安全的例子,我前面的文章Java并发编程:线程安全和ThreadLocal里面提到了,简而言之就是多个线程在同时访问或修改公共资源的时候,由于不同线程抢占公共资源而导致的结果不确定性,就是在并 ...

  4. java高并发系列 - 第12天JUC:ReentrantLock重入锁

    java高并发系列 - 第12天JUC:ReentrantLock重入锁 本篇文章开始将juc中常用的一些类,估计会有十来篇. synchronized的局限性 synchronized是java内置 ...

  5. 浅谈Java中的锁:Synchronized、重入锁、读写锁

    Java开发必须要掌握的知识点就包括如何使用锁在多线程的环境下控制对资源的访问限制 ◆ Synchronized ◆ 首先我们来看一段简单的代码: 12345678910111213141516171 ...

  6. synchronized 是可重入锁吗?为什么?

    什么是可重入锁? 关于什么是可重入锁,我们先来看一段维基百科的定义. 若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(re ...

  7. JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁,

    如果需要查看具体的synchronized和lock的实现原理,请参考:解决多线程安全问题-无非两个方法synchronized和lock 具体原理(百度) 在并发编程中,经常遇到多个线程访问同一个 ...

  8. Java 多线程 重入锁

    作为关键字synchronized的替代品(或者说是增强版),重入锁是synchronized的功能扩展.在JDK 1.5的早期版本中,重入锁的性能远远好于synchronized,但从JDK 1.6 ...

  9. synchronized的功能拓展:重入锁(读书笔记)

     重入锁可以完全代替synchronized关键字.在JDK5.0的早期版本中,重入锁的性能远远好于synchronized,但是从JDK6.0开始.JDK在synchronized上做了大量的优化. ...

随机推荐

  1. Android No static field XXX of type I in class Lcom/XXX/R$id错

    问题复现: 问题原因: 出现这样的情况,你先检查你的依赖工程(module)的对应布局layout/xxx.xml是否跟主项目的layout重名,你点开R文件的时候,你会发现你的布局发生了错乱,导致你 ...

  2. go语言从例子开始之Example24.通道同步

    我们可以使用通道来同步 Go 协程间的执行状态.这里是一个使用阻塞的接受方式来等待一个 Go 协程的运行结束. Example: package main import "fmt" ...

  3. 【串线篇】SpringMvc数据传出

    /** * SpringMVC除过在方法上传入原生的request和session外还能怎么样把数据带给页面 *  * 四大域: *  pageContext:${pageScope.msg }< ...

  4. 【硬盘】RAID

    RAID是英文Redundant Array of Independent Disks(独立磁盘冗余阵列),简称磁盘阵列.下面将各个级别的RAID介绍如下. 一.为什么使用Raid? 1.对磁盘高速存 ...

  5. 百度链接提交主动推送 c#实现

    说明:此方法适合百度站长,利用此方法可以第一时间将你的原创文章上传到百度,保护你的著作权,也可以帮你的网站进行引流 以下是代码实现: /// <summary>         /// 提 ...

  6. Network基础(六):telnet远程管理

    一.telnet远程管理 目标: 在企业中为方便网络管理员对Cisco设备的配置,一般需事先在Cisco交换机及路由器上开启远程管理的服务,借助网络通过telnet方式远程访问. 方案: 网络管理员通 ...

  7. Linux 下安装配置redis

    第一部分:安装redis 本人是先下载了4.0.2版本的redis安装包直接远程上传到linux服务器/usr/local目录下 那么安装过程如下: tar -zxvf redis-4.0.2.tar ...

  8. CF446D DZY Loves Games

    CF446D DZY Loves Games 高斯消元好题 如果暴力地,令f[i][k]表示到i,有k条命的概率,就没法做了. 转化题意 生命取决于经过陷阱的个数 把这个看成一步 所以考虑从一个陷阱到 ...

  9. java中常用的转义字符(转)

    Java编程中往往需要一些特殊操作,例如空格,换行.或者一些你使用特殊符号的意愿与程序中特殊符号意思冲突的时候,我们不能直接写就需要把这些符号转义,表达你的本意,并与程序中特殊符号做区分,这些都需要转 ...

  10. context和getApplicationContext()的区别

    在android中常常会遇到与context有关的内容 浅论一下 context : 在语句 AlertDialog.Builder builder = new AlertDialog.Builder ...