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

 

 

 

 

  

posted @ 2020-03-12 12:18  zomicc  阅读(825)  评论(0编辑  收藏  举报