使用redis实现分布式锁

使用redis实现分布式锁简介 当高并发访问某个接口的时候 如果这个接口访问的数据库中的资源 并且你的数据库事务级别是可重复读 Repeatablere 的话 确实是没有线程问题的 因为数据库锁的级别就够了 但是如果这个接口需要访问一个静态变量 静态代码块 全局缓存的中的资源或者 redis 中的资源的时候 就会出现线程安全的问题 案例 github 地址 https github com mzd123 myw

简介:

当高并发访问某个接口的时候,如果这个接口访问的数据库中的资源,并且你的数据库事务级别是可重复读(Repeatable read)的话,确实是没有线程问题的,因为数据库锁的级别就够了;但是如果这个接口需要访问一个静态变量、静态代码块、全局缓存的中的资源或者redis中的资源的时候,就会出现线程安全的问题。

案例:

github地址: https://github.com/mzd123/mywy/tree/master/src/main/java/com/mzd/mywy/service

@RestController public class MsController { @Autowired private MsService msService; @RequestMapping("/select_info.do") public String select_info(String product_id) { return msService.select_info(product_id); } @RequestMapping("/order.do") public String order(String product_id) throws CongestionException { return msService.order1(product_id); } } 
@Service public class MsService { @Autowired private RedisLock redisLock; //商品详情 private static HashMap 
  
    product = new HashMap(); //订单表 private static HashMap 
   
     orders = new HashMap(); //库存表 private static HashMap 
    
      stock = new HashMap(); static { product.put("123", 10000); stock.put("123", 10000); } public String select_info(String product_id) { return "限量抢购商品XXX共" + product.get(product_id) + ",现在成功下单" + orders.size() + ",剩余库存" + stock.get(product_id) + "件"; } / * 下单 * * @param product_id * @return */ public String order1(String product_id) { if (stock.get(product_id) == 0) { return "活动已经结束了"; //已近买完了 } else { //还没有卖完 try { //模拟操作数据库 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } orders.put(MyStringUtils.getuuid(), product_id); stock.put(product_id, stock.get(product_id) - 1); } return select_info(product_id); } } 
     
    
  

解决1: 学过javase的小伙伴应该都能想到使用synchronized关键字,强行同步。

 / * 下单 * * @param product_id * @return */ public synchronized String order2(String product_id) { if (stock.get(product_id) == 0) { return "活动已经结束了"; //已近买完了 } else { //还没有卖完 try { //模拟操作数据库 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } orders.put(MyStringUtils.getuuid(), product_id); stock.put(product_id, stock.get(product_id) - 1); } return select_info(product_id); } 

在这里插入图片描述
缺点:
1、我们可以明显的看到速度变慢了,从原来的0.535秒变到了10.956秒,那是因为synchronized放这个方法只允许单线程访问了。
2、synchronized是粗粒度的控制了线程安全,即:如果我这个商品id不一样的线程,理论上是可以同时访问这个方法的,但是加上了synchronized之后,无论商品id是否一样,两个线程都是没法同时访问这个方法的。






解决2: 使用redis分布式锁(主要使用了redis中的setnxgetset方法,这两个方法在redisTemplate分别是setIfAbsentgetAndSet方法)实现线程安全,因为redis是单线程,能保证线程的安全性,而且redis强大的读写能力能提高效率。

 / * 高并发没问题,效率还行 * * @param product_id * @return */ public String order3(String product_id) throws CongestionException { / * redis加锁 */ String value = System.currentTimeMillis() + 10000 + ""; if (!redisLock.lock1(product_id, value)) { //系统繁忙,请稍后再试 throw new CongestionException(); } //业务逻辑// if (stock.get(product_id) == 0) { return "活动已经结束了"; //已近买完了 } else { //还没有卖完 try { //模拟操作数据库 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } orders.put(MyStringUtils.getuuid(), product_id); stock.put(product_id, stock.get(product_id) - 1); } //业务逻辑// / * redis解锁 */ redisLock.unlock(product_id, value); return select_info(product_id); } 
/ * 用redis实现分布式锁 */ @Component public class RedisLock { @Autowired private StringRedisTemplate redisTemplate; //加锁 public boolean lock1(String key, String value) { //setIfAbsent相当于jedis中的setnx,如果能赋值就返回true,如果已经有值了,就返回false //即:在判断这个key是不是第一次进入这个方法 if (redisTemplate.opsForValue().setIfAbsent(key, value)) { //第一次,即:这个key还没有被赋值的时候 return true; } return false; } //解锁 public void unlock(String key, String value) { try { if (MyStringUtils.Object2String(redisTemplate.opsForValue().get(key)).equals(value)) { redisTemplate.opsForValue().getOperations().delete(key); } } catch (Exception e) { e.printStackTrace(); } } } 

缺点: 这个方法看上去没什么问题,而且只有商品id相同的两个线程同时访问这个方法的时候才会出现线程问题,这似乎是很完美了。但是有没有想过,万一处理业务逻辑的代码块中出现了异常,直接抛了出去,那解锁的代码就再也不会被执行了,也就是出现了死锁现象

改进1:

 public boolean lock2(String key, String value) { //setIfAbsent相当于jedis中的setnx,如果能赋值就返回true,如果已经有值了,就返回false //即:在判断这个key是不是第一次进入这个方法 if (redisTemplate.opsForValue().setIfAbsent(key, value)) { //第一次,即:这个key还没有被赋值的时候 return true; } String current_value = redisTemplate.opsForValue().get(key);//① if (!MyStringUtils.Object2String(current_value).equals("") //超时了 && Long.parseLong(current_value) < System.currentTimeMillis()) {;//② //返回true就能解决死锁 return true; } return false; } 

缺点: 使用超时时间来解决死锁问题,但是又出现新的问题,就是当有两个商品id相同的线程同时执行到了②这一行代码,这时候两个线程同时获取锁,这样一来任然存在线程安全问题了。。。

改进2:

 / * 加锁 */ public boolean lock3(String key, String value) { //setIfAbsent相当于jedis中的setnx,如果能赋值就返回true,如果已经有值了,就返回false //即:在判断这个key是不是第一次进入这个方法 if (redisTemplate.opsForValue().setIfAbsent(key, value)) { //第一次,即:这个key还没有被赋值的时候 return true; } String current_value = redisTemplate.opsForValue().get(key); if (!MyStringUtils.Object2String(current_value).equals("") //超时了 && Long.parseLong(current_value) < System.currentTimeMillis()) {//① String old_value = redisTemplate.opsForValue().getAndSet(key, value);//② if (!MyStringUtils.Object2String(old_value).equals("") && old_value.equals(current_value)) { return true; } } return false; } 

解释: 如果两个线程同时调用这个方法,当同时走到①的时候,无论怎么样都有一个线程会先执行②这一行,假设线程1先执行②这行代码,那redis中key对应的value就变成了value,然后线程2再执行②这行代码的时候,获取到的old_value就是value,那么value显然和他上面获取的current_value是不一样的,则线程2是没法获取锁的。
在这里插入图片描述
在这里插入图片描述
说明: 虽然100个请求只有2个成功下单的,但是耗时却明显变小了,而且线程也是安全的,只是绝大部分因为没有拿到锁而没有抢到限购的商品,但也做了人性化的提醒,个人觉得还是可以接受的!






微信公众号关注: 965净化者

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/205257.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 下午6:26
下一篇 2026年3月19日 下午6:26


相关推荐

  • 直方图阈值法

    直方图阈值法直方图阈值法用 MATLAB 实现直方图阈值法 clc clear close I imread e role0 003i bmp I1 rgb2gray I figure subplot 2 2 1 imshow I1 title 灰度图像 gridon 显示网格线 axison 显示坐标系 m n siz

    2026年3月26日
    2
  • formidable接收图片

    formidable接收图片DOCTYPE tml htmllang en head metacharset UTF 8 metaname viewport content width device width initial scale 1 0 metahttp equiv X UA Compatible content ie edge title Do title metahttp equiv X UA Compatible content ie edge metaname viewport content width device width initial scale 1 0 metacharset UTF 8 head htmllang en

    2026年3月17日
    2
  • Sql Server数据恢复

    Sql Server数据恢复

    2021年8月2日
    62
  • mysql修改root用户密码语法为_设置mysql的root密码

    mysql修改root用户密码语法为_设置mysql的root密码知道密码在清楚的知道密码的情况之下可以使用以下几种方式修改MySQL的密码。方式一登录mysql

    2022年8月13日
    7
  • 一致性哈希算法 Java实现

    一致性哈希算法 Java实现一致性哈希算法虚拟节点Java版本的简单实现。

    2022年7月27日
    8
  • CPU寻址方式

    CPU寻址方式nbsp nbsp nbsp nbsp 汇编语言的语法是指令 指令目的操作数 源操作数 需要处理的数据 立即数 地址 寄存器存放的数据等 称为源操作数 而指令处理结果的存放目的地称为指令目的操作数 寄存器 地址等 而处理器是根据地址从存储单元中取出指令来执行的 根据 CPU 访问数据 寻址 形式的不同划分了以下几种寻址方式 寻址方式 寄存器寻址 立即数寻址 内存寻址 直接寻址 基址寻址 变址寻

    2026年3月18日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号