死磕 java同步系列之redis分布式锁进化史 (3)

配置redis的连接信息,彤哥这里给出了三种方式。

spring: redis: # 单机模式 #host: 192.168.1.102 #port: 6379 # password: <your passowrd> timeout: 6000ms # 连接超时时长(毫秒) # 哨兵模式 【本篇文章由公众号“彤哥读源码”原创】 # sentinel: # master: <your master> # nodes: 192.168.1.101:6379,192.168.1.102:6379,192.168.1.103:6379 # 集群模式(三主三从伪集群) cluster: nodes: - 192.168.1.102:30001 - 192.168.1.102:30002 - 192.168.1.102:30003 - 192.168.1.102:30004 - 192.168.1.102:30005 - 192.168.1.102:30006 Locker接口

定义Locker接口。

public interface Locker { void lock(String key, Runnable command); } RedisLocker实现类

直接使用RedissonClient获取锁,注意这里不需要再单独配置RedissonClient这个bean,redisson框架会根据配置自动生成RedissonClient的实例,我们后面说它是怎么实现的。

@Component public class RedisLocker implements Locker { @Autowired private RedissonClient redissonClient; @Override public void lock(String key, Runnable command) { RLock lock = redissonClient.getLock(key); try { // 【本篇文章由公众号“彤哥读源码”原创】 lock.lock(); command.run(); } finally { lock.unlock(); } } } 测试类

启动1000个线程,每个线程内部打印一句话,然后睡眠1秒。

@RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) public class RedisLockerTest { @Autowired private Locker locker; @Test public void testRedisLocker() throws IOException { for (int i = 0; i < 1000; i++) { new Thread(()->{ locker.lock("lock", ()-> { // 可重入锁测试 locker.lock("lock", ()-> { System.out.println(String.format("time: %d, threadName: %s", System.currentTimeMillis(), Thread.currentThread().getName())); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); }); }, "Thread-"+i).start(); } System.in.read(); } }

运行结果:

可以看到稳定在1000ms左右打印一句话,说明这个锁是可用的,而且是可重入的。

time: 1570100167046, threadName: Thread-756 time: 1570100168067, threadName: Thread-670 time: 1570100169080, threadName: Thread-949 time: 1570100170093, threadName: Thread-721 time: 1570100171106, threadName: Thread-937 time: 1570100172124, threadName: Thread-796 time: 1570100173134, threadName: Thread-944 time: 1570100174142, threadName: Thread-974 time: 1570100175167, threadName: Thread-462 time: 1570100176180, threadName: Thread-407 time: 1570100177194, threadName: Thread-983 time: 1570100178206, threadName: Thread-982 ... RedissonAutoConfiguration

刚才说RedissonClient不需要配置,其实它是在RedissonAutoConfiguration中自动配置的,我们简单看下它的源码,主要看redisson()这个方法:

@Configuration @ConditionalOnClass({Redisson.class, RedisOperations.class}) @AutoConfigureBefore(RedisAutoConfiguration.class) @EnableConfigurationProperties({RedissonProperties.class, RedisProperties.class}) public class RedissonAutoConfiguration { @Autowired private RedissonProperties redissonProperties; @Autowired private RedisProperties redisProperties; @Autowired private ApplicationContext ctx; @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean(StringRedisTemplate.class) public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean(RedisConnectionFactory.class) public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redisson) { return new RedissonConnectionFactory(redisson); } @Bean(destroyMethod = "shutdown") @ConditionalOnMissingBean(RedissonClient.class) public RedissonClient redisson() throws IOException { Config config = null; Method clusterMethod = ReflectionUtils.findMethod(RedisProperties.class, "getCluster"); Method timeoutMethod = ReflectionUtils.findMethod(RedisProperties.class, "getTimeout"); Object timeoutValue = ReflectionUtils.invokeMethod(timeoutMethod, redisProperties); int timeout; if(null == timeoutValue){ // 超时未设置则为0 timeout = 0; }else if (!(timeoutValue instanceof Integer)) { // 转毫秒 Method millisMethod = ReflectionUtils.findMethod(timeoutValue.getClass(), "toMillis"); timeout = ((Long) ReflectionUtils.invokeMethod(millisMethod, timeoutValue)).intValue(); } else { timeout = (Integer)timeoutValue; } // 看下是否给redisson单独写了一个配置文件 if (redissonProperties.getConfig() != null) { try { InputStream is = getConfigStream(); config = Config.fromJSON(is); } catch (IOException e) { // trying next format try { InputStream is = getConfigStream(); config = Config.fromYAML(is); } catch (IOException e1) { throw new IllegalArgumentException("Can't parse config", e1); } } } else if (redisProperties.getSentinel() != null) { // 如果是哨兵模式 Method nodesMethod = ReflectionUtils.findMethod(Sentinel.class, "getNodes"); Object nodesValue = ReflectionUtils.invokeMethod(nodesMethod, redisProperties.getSentinel()); String[] nodes; // 看sentinel.nodes这个节点是列表配置还是逗号隔开的配置 if (nodesValue instanceof String) { nodes = convert(Arrays.asList(((String)nodesValue).split(","))); } else { nodes = convert((List<String>)nodesValue); } // 生成哨兵模式的配置 config = new Config(); config.useSentinelServers() .setMasterName(redisProperties.getSentinel().getMaster()) .addSentinelAddress(nodes) .setDatabase(redisProperties.getDatabase()) .setConnectTimeout(timeout) .setPassword(redisProperties.getPassword()); } else if (clusterMethod != null && ReflectionUtils.invokeMethod(clusterMethod, redisProperties) != null) { // 如果是集群模式 Object clusterObject = ReflectionUtils.invokeMethod(clusterMethod, redisProperties); Method nodesMethod = ReflectionUtils.findMethod(clusterObject.getClass(), "getNodes"); // 集群模式的cluster.nodes是列表配置 List<String> nodesObject = (List) ReflectionUtils.invokeMethod(nodesMethod, clusterObject); String[] nodes = convert(nodesObject); // 生成集群模式的配置 config = new Config(); config.useClusterServers() .addNodeAddress(nodes) .setConnectTimeout(timeout) .setPassword(redisProperties.getPassword()); } else { // 单机模式的配置 config = new Config(); String prefix = "redis://"; Method method = ReflectionUtils.findMethod(RedisProperties.class, "isSsl"); // 判断是否走ssl if (method != null && (Boolean)ReflectionUtils.invokeMethod(method, redisProperties)) { prefix = "rediss://"; } // 生成单机模式的配置 config.useSingleServer() .setAddress(prefix + redisProperties.getHost() + ":" + redisProperties.getPort()) .setConnectTimeout(timeout) .setDatabase(redisProperties.getDatabase()) .setPassword(redisProperties.getPassword()); } return Redisson.create(config); } private String[] convert(List<String> nodesObject) { // 将哨兵或集群模式的nodes转换成标准配置 List<String> nodes = new ArrayList<String>(nodesObject.size()); for (String node : nodesObject) { if (!node.startsWith("redis://") && !node.startsWith("rediss://")) { nodes.add("redis://" + node); } else { nodes.add(node); } } return nodes.toArray(new String[nodes.size()]); } private InputStream getConfigStream() throws IOException { // 读取redisson配置文件 Resource resource = ctx.getResource(redissonProperties.getConfig()); InputStream is = resource.getInputStream(); return is; } }

网上查到的资料中很多配置都是多余的(可能是版本问题),看下源码很清楚,这也是看源码的一个好处。

总结

(1)redis由于历史原因导致有三种模式:单机、哨兵、集群;

(2)redis实现分布式锁的进化史:set -> setnx -> setnx + setex -> set nx ex(或px) -> set nx ex(或px) + lua script -> redisson;

(3)redis分布式锁有现成的轮子redisson可以使用;

(4)redisson还提供了很多有用的组件,比如分布式集合、分布式同步器、分布式对象;

彩蛋

redis分布式锁有哪些优点?

答:1)大部分系统都依赖于redis做缓存,不需要额外依赖其它组件(相对于zookeeper来说);

2)redis可以集群部署,相对于mysql的单点更可靠;

3)不会占用mysql的连接数,不会增加mysql的压力;

4)redis社区相对活跃,redisson的实现更是稳定可靠;

5)利用过期机制解决客户端断线的问题,虽然不太及时;

6)有现成的轮子redisson可以使用,锁的种类比较齐全;

redis分布式锁有哪些缺点?

答:1)集群模式下会在所有master节点执行加锁命令,大部分(2N+1)成功了则获得锁,节点越多,加锁的过程越慢;

2)高并发情况下,未获得锁的线程会睡眠重试,如果同一把锁竞争非常激烈,会占用非常多的系统资源;

3)历史原因导致的坑挺多的,自己很难实现出来健壮的redis分布式锁;

总之,redis分布式锁的优点是大于缺点的,而且社区活跃,这也是我们大部分系统使用redis作为分布式锁的原因。

推荐阅读

1、死磕 java同步系列之开篇

2、死磕 java魔法类之Unsafe解析

3、死磕 java同步系列之JMM(Java Memory Model)

4、死磕 java同步系列之volatile解析

5、死磕 java同步系列之synchronized解析

6、死磕 java同步系列之自己动手写一个锁Lock

7、死磕 java同步系列之AQS起篇

8、死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁

9、死磕 java同步系列之ReentrantLock源码解析(二)——条件锁

10、死磕 java同步系列之ReentrantLock VS synchronized

11、死磕 java同步系列之ReentrantReadWriteLock源码解析

12、死磕 java同步系列之Semaphore源码解析

13、死磕 java同步系列之CountDownLatch源码解析

14、死磕 java同步系列之AQS终篇

15、死磕 java同步系列之StampedLock源码解析

16、死磕 java同步系列之CyclicBarrier源码解析

17、死磕 java同步系列之Phaser源码解析

18、死磕 java同步系列之mysql分布式锁

19、死磕 java同步系列之zookeeper分布式锁

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpffjj.html