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 }
二、基于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 }
三、使用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>
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>
测试代码:
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 }
四、原子性优化
以上方式[包含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 }
在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 }
There are two things to do in a day: a happy thing and a difficult one.