zoukankan      html  css  js  c++  java
  • redis实现分布式锁的两种方式

      使用数据库写锁、synchronized、ReentrantLock等都可以实现对于数据的线程安全控制。但这些都属于排它锁(或者你也可以认为是悲观锁)范畴,会造成一定的阻塞,无法满足快速响应的要求。


    基于【高并发抢购防止超卖】的案例。                    

    我们使用redis的两种不同方式,实现分布式锁。              

    【阅读前提:您对redis中的watch、事务、setnx有一定的了解】


    一、基于watch机制

      这种相当于是乐观锁的实现方式,乐观的以为没人和我抢。乐观锁适用于“读多写少”的场景。此处仅作为练习使用。方式二才是通常用法。

     1 package qianggou;
     2 
     3 import java.util.List;
     4 import java.util.UUID;
     5 import java.util.concurrent.ExecutorService;
     6 import java.util.concurrent.Executors;
     7 
     8 import comm.Value;
     9 import redis.clients.jedis.Jedis;
    10 import redis.clients.jedis.JedisPool;
    11 import redis.clients.jedis.JedisPoolConfig;
    12 import redis.clients.jedis.Transaction;
    13 
    14 /**
    15  * 测试抢购案例 
    16  *
    17  */
    18 public class RedisTest {
    19 
    20     public static void main(String[] args) {
    21         final String watchkeys = "watchkeys";
    22         ExecutorService excutor = Executors.newFixedThreadPool(20);//开启最多20个线程的线程池,相当于真实场景中的限流
    23         JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    24         JedisPool jedisPool = new JedisPool(jedisPoolConfig, "aliyun", 6379, 5000, Value.PASSWORD);//Value.PASSWORD是你的redis设置密码,这里我使用接口常量封装了
    25         
    26         final Jedis jedis = jedisPool.getResource();
    27         jedis.set(watchkeys, "0");
    28         jedis.del("setsucc","setfail");
    29         jedis.close();
    30         
    31         
    32         for(int i=0;i<1000;i++) {
    33             excutor.execute(new MyRunnable(jedisPool));
    34         }
    35         excutor.shutdown();
    36     }
    37 }
    38 class MyRunnable implements Runnable{
    39 
    40     String watchkeys = "watchkeys";
    41     JedisPool jedisPool = null;
    42     
    43     public MyRunnable(JedisPool jedisPool) {
    44         this.jedisPool = jedisPool;
    45     }
    46     @Override
    47     public void run() {
    48         Jedis jedis = jedisPool.getResource();
    49         jedis.watch(watchkeys);//监听watchkeys
    50         String val = jedis.get(watchkeys);
    51         jedis.set(watchkeys, "1");
    52         int valint = Integer.valueOf(val);
    53         String userinfo = UUID.randomUUID().toString();
    54         if(valint < 10) {
    55             
    56             Transaction tx = jedis.multi();
    57             tx.incr(watchkeys);//更改watchkeys的值。
    58             List<Object> exec = tx.exec();//如果watchkeys被其他线程修改了,则抢购失败
    59             if(exec !=null && exec.size()>0) {
    60                 System.out.println("用户【"+userinfo+"】抢购成功,当前抢购成功人数为:"+(valint+1));
    61                 jedis.sadd("setsucc", userinfo);
    62                 jedis.close();
    63                 return;
    64             }
    65         }
    66         jedis.sadd("setfail", userinfo);
    67         jedis.close();
    68     }
    69 }
    View Code

    二、基于setnx

      这种相当于是悲观锁的实现方式,没有获取到锁则抢购失败。(其实真实的悲观锁是会进行等待阻塞的)

     1 package qianggou;
     2 
     3 import java.util.UUID;
     4 import java.util.concurrent.ExecutorService;
     5 import java.util.concurrent.Executors;
     6 import java.util.concurrent.ScheduledExecutorService;
     7 import java.util.concurrent.TimeUnit;
     8 
     9 import comm.Value;
    10 import redis.clients.jedis.Jedis;
    11 import redis.clients.jedis.JedisPool;
    12 import redis.clients.jedis.JedisPoolConfig;
    13 
    14 public class RedisTest02 {
    15 
    16     
    17     public static void main(String[] args) {
    18         final String SAIL_KEY = "sailkey";
    19         ExecutorService excutor = Executors.newFixedThreadPool(20);
    20         
    21         JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    22         JedisPool jedisPool = new JedisPool(jedisPoolConfig, "aliyun", 6379, 5000, Value.PASSWORD);
    23         
    24         final Jedis jedis = jedisPool.getResource();
    25         jedis.del(SAIL_KEY);
    26         jedis.set(SAIL_KEY, "0");
    27         jedis.del("setsucc","setfail","LOCK");
    28         jedis.close();
    29         
    30         for(int i=0;i<100;i++) {
    31             excutor.execute(new MyRunnable2(jedisPool));
    32         }
    33         excutor.shutdown();
    34     }
    35 
    36 }
    37 class MyRunnable2 implements Runnable{
    38 
    39     String LOCK = "LOCK";
    40     String SAIL_KEY = "sailkey";
    41     JedisPool jedisPool  = null;
    42     
    43     public MyRunnable2(JedisPool jedisPool ) {
    44         this.jedisPool = jedisPool;
    45     }
    46     @Override
    47     public void run() {
    48         
    49         String userinfo = UUID.randomUUID().toString();
    50         
    51         Jedis jedis = jedisPool.getResource();
    52 
    53         Long lock = jedis.setnx(LOCK, userinfo);
    54         if(lock>0 ? false : true) {//这一步在spring中封装为RedisTemplate了
    55             return;
    56         }    
    57         
    58         ScheduledExecutorService schedul = Executors.newScheduledThreadPool(1);
    59         try {
    60             jedis.expire(LOCK, 2);//初始化锁定时间
    61             
    62             //异常点一:设置锁的初始锁定时间,如果2秒钟之内方法未执行完,则通过以下定时器为锁续命
    63             schedul.schedule(new Runnable() {
    64                 @Override
    65                 public void run() {
    66                     jedis.expire(LOCK, 2); 
    67                 }
    68             }, 1, TimeUnit.SECONDS);//定时每1秒为锁续期
    69             
    70             int num = jedis.incr(SAIL_KEY).intValue();
    71             
    72             if(num<=10) {
    73                 System.out.println("用户【"+userinfo+"】抢购成功,当前抢购成功人数为:"+num);
    74             }
    75         }finally {
    76             schedul.shutdownNow();//关闭所有定时子进程
    77             //异常点二:如果执行到这一步,突然卡住了Thread.sleep(),LOCK时间也到期了。
    78             //别的线程就可以重新生成LOCK这把锁,防止以下代码删除了别的线程的LOCK
    79             if(userinfo.equals(jedis.get(LOCK))) {//防止误删
    80                 jedis.del(LOCK); //方式一
    81             }
    82             jedis.close();//先删除再关闭
    83         }
    84     }
    85     
    86 }
    View Code

    三、使用redisson进行简化

      方案二中的加锁看起来过于繁琐了,接下来使用redisson对其加锁续期过程进行简化。我们使用一下spring中的RedisTemplate(你也可以不使用)。

      pom中需要引入引用:

     1 <dependency>
     2         <groupId>org.springframework.data</groupId>
     3         <artifactId>spring-data-redis</artifactId>
     4         <version>1.7.7.RELEASE</version>
     5  </dependency>
     6 <dependency>
     7     <groupId>org.redisson</groupId>
     8     <artifactId>redisson</artifactId>
     9     <version>3.11.3</version>
    10 </dependency>
    View Code

      spring配置文件:

     1 <?xml version="1.0" encoding="UTF-8"?>
     2 <beans xmlns="http://www.springframework.org/schema/beans"
     3     xmlns:context="http://www.springframework.org/schema/context"
     4     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     5     xsi:schemaLocation="http://www.springframework.org/schema/beans
     6      http://www.springframework.org/schema/beans/spring-beans.xsd
     7      http://www.springframework.org/schema/context
     8      http://www.springframework.org/schema/context/spring-context.xsd">
     9 
    10     <bean id="redisPoolConfig"
    11         class="redis.clients.jedis.JedisPoolConfig"></bean>
    12 
    13     <bean id="JedisConnectionFactory"
    14         class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    15         <property name="hostName" value="127.0.0.1"></property>
    16         <property name="port" value="6379"></property>
    17         <property name="password" value="123456"></property>
    18         <property name="poolConfig" ref="redisPoolConfig"></property>
    19     </bean>
    20     
    21     <bean id="stringRedisSerializer"
    22         class="org.springframework.data.redis.serializer.StringRedisSerializer" />
    23     <bean id="redisTemplate"
    24         class="org.springframework.data.redis.core.RedisTemplate">
    25         <property name="connectionFactory" ref="JedisConnectionFactory" />
    26         <property name="keySerializer" ref="stringRedisSerializer" />
    27         <property name="valueSerializer" ref="stringRedisSerializer" />
    28     </bean>
    29 
    30 </beans>
    View Code

      测试代码:

     1 package test;
     2 
     3 import java.util.UUID;
     4 import java.util.concurrent.ExecutorService;
     5 import java.util.concurrent.Executors;
     6 
     7 import org.redisson.Redisson;
     8 import org.redisson.api.RLock;
     9 import org.redisson.api.RedissonClient;
    10 import org.redisson.config.Config;
    11 import org.springframework.context.ApplicationContext;
    12 import org.springframework.context.support.ClassPathXmlApplicationContext;
    13 import org.springframework.data.redis.core.RedisTemplate;
    14 
    15 public class Miaosha2 {
    16     
    17     public static void main(String[] args) {
    18         
    19         ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext-redis.xml");
    20         RedisTemplate<String, String> redisTemplate = ac.getBean(RedisTemplate.class);
    21         //数据初始化
    22         redisTemplate.opsForValue().set("sails", "0");//初始化库存已售数量
    23         redisTemplate.delete("LOCK");
    24         
    25         System.out.println(redisTemplate);
    26         
    27         ExecutorService excutor = Executors.newFixedThreadPool(20);
    28         for(int i=0;i<100;i++) {    
    29             excutor.execute(new MyRunnable2(redisTemplate));
    30         }
    31         excutor.shutdown();
    32     
    33     }
    34 
    35 }
    36 class MyRunnable2 implements Runnable{
    37     RedisTemplate<String, String> redisTemplate;
    38     public MyRunnable2(RedisTemplate<String, String> redisTemplate) {
    39         this.redisTemplate = redisTemplate;
    40     }
    41     @Override
    42     public void run() {
    43         String userInfo = UUID.randomUUID().toString();
    44         Config config = new Config();
    45         config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    46         config.useSingleServer().setPassword("123456");
    47 
    48         RedissonClient redisson = Redisson.create(config);
    49         RLock lock = redisson.getLock("LOCK");
    50         
    51         try {
    52             lock.lock();//获取锁,获取不到则结束
    53 
    54         redisTemplate.opsForValue().set("sails", String.valueOf(num));
    55             Long num = redisTemplate.opsForValue().increment("sails", 1);
    56             if(num <= 10) {
    57                 System.out.println("用户【" + userInfo + "】抢购成功,当前抢购成功人数为:" + num);
    58             }
    59         } finally {
    60             lock.unlock();//释放锁
    61             redisson.shutdown();//需要关闭redisson连接,否则会占用redis连接资源
    62         }
    63         
    64     }
    65     
    66 }
    View Code

     四、原子性优化

      以上方式[包含redisson方式]外存在一个很严重的额问题是,设置锁和设置超时时间的代码是非原子性操作。

      我们想象一个场景如果设置完锁之后系统异常宕机,但是没有设置超时时间,这导致的后果就是:锁永远无法释放了

      为了保证这两步的原子性操作===>,

      在redis2.6.12版本之前,可以使用LUA脚本方式: 

     1 public class Lua {
     2     
     3     //加锁脚本(索引从1开始)
     4     private static final String SCRIPT_TRYLOCK = "if redis.call('setnx',KEYS[1],ARGV[1]) ==1 "
     5             + " then redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
     6     
     7     /**
     8      * 使用Lua脚本,尝试获取分布式锁
     9      */
    10     public static boolean tryLockLua(Jedis jedis, String lockkey, String lockValue, int expireTime) {
    11         
    12         int result = (int) jedis.eval(SCRIPT_TRYLOCK, 1, lockkey,String.valueOf(expireTime));//设置锁
    13         if(result == 1) {
    14             return true ;
    15         }
    16         return false;
    17     }
    18 }
    View Code

      在redis2.6.12版本及其之后,对set命令进行了增强,同时作者也不建议使用setnx命令了,这个命令也没存在的必要了,后续版本可能会将其删除:

     1 /**
     2      * 命令:SET key value NX PX miliseconds
     3      *    如:SET key value NX PX 1000
     4      */
     5     public static boolean tryLock(Jedis jedis, String lockkey, String lockValue, int expireTime) {
     6         
     7         String result = jedis.set(lockkey, lockValue,"NX","PX",expireTime);
     8         if("OK".equals(result)) {
     9             return true ;
    10         }
    11         return false;
    12     }
    View Code

      

    "我们所要追求的,永远不是绝对的正确,而是比过去的自己更好"
  • 相关阅读:
    matlab中figure 创建图窗窗口
    matlab中imread 从图形文件读取图像
    matlab中imfinfo 有关图形文件的信息
    matlab中bitshift 将位移动指定位数
    matlab中reshape 重构数组
    matlab中find 查找非零元素的索引和值
    比特数
    matlab中fseek 移至文件中的指定位置
    poj 1039 Pipe(几何基础)
    poj 1556 The Doors(线段相交,最短路)
  • 原文地址:https://www.cnblogs.com/zomicc/p/12468324.html
Copyright © 2011-2022 走看看