分布式锁主要用于解决,公司中不一样业务系统对同一功能的数据产生脏读或重复插入。html
好比公司现有三个小组分别开发WAP站、小程序、APP客户端,而这三个系统都存在领红包功能。java
业务要求每人每日只能领取一个红包,若是有人同时登录三个系统那么就可以同一时间领取到三个红包。git
分布式锁要知足如下基本要求:github
本例参考了博文:https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/redis
例子已上传码云:https://gitee.com/imlichao/jedis-distributed-lock-examplespring
本例使用spring boot提供的redis实现,并无直接引入jedis依赖。这样作的好处是,能够在项目中同时使用Jedis和RedisTemplate实例。数据库
pom.xml文件小程序
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> </dependency>
application.properties文件缓存
#redis spring.redis.database=0 spring.redis.host=18.6.8.22 spring.redis.password=Mfqy_redis_password_ spring.redis.port=6379 #链接超时时间(项目或链接池连接redis超时的时间) spring.redis.timeout=2000 #最大链接数(建议为 业务指望QPS/单个链接的QPS,50000/1000=50) spring.redis.pool.max-active=50 #最大空闲连接数(为减少伸缩产生的性能消耗,建议和最大链接数设成一致的) spring.redis.pool.max-idle=50 #最小空闲链接数(0表明在无请求的情况下从不建立连接) spring.redis.pool.min-idle=0 #链接池占满后没法获取链接时的阻塞时间(超时后抛出异常) spring.redis.pool.max-wait=3000
因为咱们使用了spring boot提供的redis实现,因此咱们不能直接获取到jedis对象。Jedis工厂类从RedisConnectionFactory中获取Redis链接(JedisConnection实现类),而后使用反射的方法从中取得了Jedis实例。服务器
/** * Jedis工厂类(单例模式) */ @Service public class JedisFactory { @Autowired private RedisConnectionFactory connectionFactory; private JedisFactory(){} private static Jedis jedis; /** * 得到jedis对象 */ public Jedis getJedis() { //从RedisConnectionFactory中获取Redis链接(JedisConnection实现类),而后使用反射的方法从中取得了Jedis实例 if(jedis == null){ Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis"); ReflectionUtils.makeAccessible(jedisField); jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection()); } return jedis; } }
为避免死锁的发生,加锁和设定失效时间必须是一个原子性操做。不然一旦在加锁后程序出错,没可以执行设置失效时间的方法时,就会产生死锁。 可是RedisTemplate屏蔽了插入数据和设置失效时间同时执行的方法,咱们只能获取到Jedis实例来执行。
分布式锁主要实现了两个方法即占用锁和释放锁。
这里须要注意占用锁和释放锁都要保证原子性操做,避免程序异常时产生死锁。
锁id主要用于标识持有锁的请求,在释放琐时用来判断只有持有正确锁id的请求才能执行解锁操做。
/** * redis分布式锁 */ @Service public class DistributedLock { @Autowired private JedisFactory JedisFactory; /** * 占用锁 * @param lockKey 锁key * @return 锁id */ public String occupyDistributedLock(String lockKey) { //得到jedis实例 Jedis jedis = JedisFactory.getJedis(); //锁id(必须拥有此id才能释放锁) String lockId = UUID.randomUUID().toString(); //占用锁同时设置失效时间 String isSuccees = jedis.set(lockKey, lockId, "NX","PX", 15000); //占用锁成功返回锁id,不然返回null if("OK".equals(isSuccees)){ return lockId; }else{ return null; } } /** * 释放锁 * @param lockKey 锁key * @param lockId 加锁id */ public void releaseDistributedLock(String lockKey,String lockId) { if(lockId != null){ //得到jedis实例 Jedis jedis = JedisFactory.getJedis(); //执行Lua代码删除lockId匹配的锁 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockId)); } } }
解释一下jedis.set(lockKey, lockId, "NX","PX", 15000)方法
格式 - String set(String key, String value, String nxxx, String expx, long time); |
Controller
/** * 分布式锁测试类 */ @Controller public class DistributedLockController { @Autowired private DistributedLock distributedLock; @RequestMapping(value = "/", method = RequestMethod.GET) public String index(){ return "/index"; } @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET) public String occupyDistributedLock(RedirectAttributes redirectAttributes, HttpServletRequest request){ String key = "userid:55689"; String lockId = null; try{ //占用锁 lockId = distributedLock.occupyDistributedLock(key); if(lockId != null){ //程序执行 TimeUnit.SECONDS.sleep(10); } }catch (Exception e){ e.printStackTrace(); }finally { //释放锁 distributedLock.releaseDistributedLock(key,lockId); } redirectAttributes.addFlashAttribute("lockId",lockId); return "redirect:/"; } }
页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>distributed lock</title> </head> <body> <h1>分布式锁测试</h1> <button onclick="window.location.href = '/occupyDistributedLock'">占用锁</button> <br/> <!-- 占用成功返回锁id --> <#if lockId??>${lockId}</#if> </body> </html>
使用Redisson提供的分布式锁更加方便,并且锁的具体细节也不须要考虑。
例子已上传码云:https://gitee.com/imlichao/redisson-distributed-lock-example
官网:https://redisson.org/
文档:https://github.com/redisson/redisson/wiki
添加依赖
spring boot 中引用专用依赖,会自动生成配置和spring bean的实例。
pom.xml文件
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.10.1</version> </dependency>
配置文件
application.properties文件
#redisson spring.redis.database=0 spring.redis.host=13.6.8.1 spring.redis.password=Mfqy_redis spring.redis.port=6379
测试代码
Controller
/** * 分布式锁测试类 */ @Controller public class DistributedLockController { @Autowired private RedissonClient redisson ; @RequestMapping(value = "/", method = RequestMethod.GET) public String index(){ return "/index"; } @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET) public String occupyDistributedLock(RedirectAttributes redirectAttributes){ RLock lock = null; try{ //锁的key String key = "MF:DISTRIBUTEDLOCK:S:personId_1001"; //得到分布式锁实例 lock = redisson.getLock(key); //加锁而且设置自动失效时间15秒 lock.lock(15, TimeUnit.SECONDS); //程序执行 TimeUnit.SECONDS.sleep(10); //获取网络时间(多服务器测试统一时间) URL url=new URL("http://www.baidu.com"); URLConnection conn=url.openConnection(); conn.connect(); long dateL=conn.getDate(); Date date=new Date(dateL); //打印和返回结果 System.out.println(date); redirectAttributes.addFlashAttribute("success",date); }catch (Exception e){ e.printStackTrace(); }finally { //释放锁 if (lock != null) lock.unlock(); } return "redirect:/"; } }
页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>distributed lock</title> </head> <body> <h1>分布式锁测试</h1> <button onclick="window.location.href = '/occupyDistributedLock'">占用锁</button> <br/> <#if success??>${success?datetime}</#if> </body> </html>
添加依赖
pom.xml文件
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.10.1</version> </dependency>
配置文件
application.properties文件
#redisson spring.redis.database=0 spring.redis.host=13.6.8.1 spring.redis.password=Mfqy_redis spring.redis.port=6379
配置类
/** * Redisson配置 */ @Configuration public class RedissonConfig { @Value("${spring.redis.database}") private int database; @Value("${spring.redis.host}") private String host; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.port}") private String port; @Bean RedissonClient createConfig() { Config config = new Config(); //设置编码方式为 Jackson JSON 编码(不设置默认也是这个) config.setCodec(new JsonJacksonCodec()); //云托管模式设置(咱们公司用的阿里云redis产品) config.useReplicatedServers() //节点地址设置 .addNodeAddress("redis://"+host+":"+port) //密码 .setPassword(password) //数据库编号(默认0) .setDatabase(database); RedissonClient redisson = Redisson.create(config); return redisson; } }
测试代码与SpringBoot配置同样