一:前言
我在实际环境中遇到了这样一种问题,分布式生成id的问题!因为业务逻辑的问题,我有个生成id的方法,是根据业务标识+id 当做唯一的值!
而uuid是递增生成的,从1开始一直递增,那么在同一台机器上运行代码,加上同步方法(synchronized),这个生成id的方法就是ok!
但是因为业务扩展或者说为了安全,项目运行在两台机器上,此时单个的同步方法(synchronized或者Lock)就不能防止id的重复了!!!
要解决上面的这个问题,其他有如下解决办法!
(1):每台机器生产Id的代码,key+id 可以在前加上机器编号区分,key + id — >机器唯一编号 + key + id
(2):使用数据库行锁(单个数据库的是时候,如何是分布式数据库也会出现问题),在需要插入id的表加上行锁,防止数据重复导致程序异常!
(3):使用分布式锁
二:分布式锁简介
网上有很多的讲解分布式锁的文章,但是细细分析很多的代码还是有很多的问题的,如下代码片段摘自博文:
和
<span style="color:#000000"><code><span style="color:#000088">public</span> <span style="color:#000088">void</span> <span style="color:#009900">lock</span>(<span style="color:#000088">long</span> timeout) {
<span style="color:#000088">long</span> nano = System.nanoTime();
timeout *= <span style="color:#006666">1000000</span>;
<span style="color:#000088">final</span> Random r = <span style="color:#000088">new</span> Random();
<span style="color:#000088">try</span> {
<span style="color:#000088">while</span> ((System.nanoTime() - nano) < timeout) {
<span style="color:#000088">if</span> (redisTemplate.getConnectionFactory().getConnection().setNX(key.getBytes(), LOCKED.getBytes())) {
redisTemplate.expire(key, EXPIRE, TimeUnit.SECONDS);
locked = <span style="color:#000088">true</span>;
logger.debug(<span style="color:#009900">"add RedisLock["</span> + key + <span style="color:#009900">"]."</span>);
<span style="color:#000088">break</span>;
}
Thread.sleep(<span style="color:#006666">3</span>, r.nextInt(<span style="color:#006666">500</span>));
}
} <span style="color:#000088">catch</span> (Exception e) {
}
}
</code></span>
上面的代码博主也说了:如果长时间获取不到,就会获取锁失败,相当于没加锁!
这里还有可能发生其他问题:
(1)并发情况,expire主动释放锁的时候,可能释放的是别人的锁(不懂请自行查询相关资料)
(2)Redis服务挂掉,锁失败,相当于没加锁!最好使用主从+哨兵提高 高可用
注:使用的时候要注意上面问题!!!
还有一种摘自博文:
这个博问分析的:
<span style="color:#000000"><code> <span style="color:#000088">while</span> (timeout >= <span style="color:#006666">0</span>) {
long expires = System.currentTimeMillis() + expireMsecs + <span style="color:#006666">1</span>;
<span style="color:#4f4f4f">String</span> expiresStr = <span style="color:#4f4f4f">String</span>.valueOf(expires); <span style="color:#880000">//锁到期时间</span>
<span style="color:#000088">if</span> (<span style="color:#000088">this</span>.setNX(lockKey, expiresStr)) {
<span style="color:#880000">// lock acquired</span>
locked = <span style="color:#006666">true</span>;
<span style="color:#000088">return</span> <span style="color:#006666">true</span>;
}
<span style="color:#4f4f4f">String</span> currentValueStr = <span style="color:#000088">this</span>.get(lockKey); <span style="color:#880000">//redis里的时间</span>
<span style="color:#000088">if</span> (currentValueStr != <span style="color:#006666">null</span> && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
<span style="color:#880000">//判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的</span>
<span style="color:#880000">// lock is expired</span>
<span style="color:#4f4f4f">String</span> oldValueStr = <span style="color:#000088">this</span>.getSet(lockKey, expiresStr);
<span style="color:#880000">//获取上一个锁到期时间,并设置现在的锁到期时间,</span>
<span style="color:#880000">//只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的</span>
<span style="color:#000088">if</span> (oldValueStr != <span style="color:#006666">null</span> && oldValueStr.equals(currentValueStr)) {
<span style="color:#880000">//防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受</span>
<span style="color:#880000">//[分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁</span>
<span style="color:#880000">// lock acquired</span>
locked = <span style="color:#006666">true</span>;
<span style="color:#000088">return</span> <span style="color:#006666">true</span>;
}
}
timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;
<span style="color:#880000">/*
延迟100 毫秒, 这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
使用随机的等待时间可以一定程度上保证公平性
*/</span>
Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
}
</code></span>
这个相比第一个完善了误删除key的问题,但是要合理的设置超时时间 (要了解具体加锁的业务),否则的话,也会使锁失效。
三:Redisson分布式锁的介绍和简单的使用
Redisson的介绍可以到:https://github.com/redisson/redisson/wiki/1.-%E6%A6%82%E8%BF%B0 这里去了解!
我这里说一下使用时候要注意的问题:
1:文档里面说明了支持Redis 2.8以上版本,支持Java1.6+以上版本。根据自己的环境选择合适的版本!
2:2.8.1的redisson 需要使用 netty的jar包, 否则报错:Hopper: java.lang.NoClassDefFoundError: io/netty/channel/EventLoopGroup。
3:2.8.1的redisson需要jackson 2.5+版本,否则报错bjectMapper.addMixIn method not fond。
我写了一个简单的例子,自己也做了一下测试,使用的Redis主从+哨兵模式!
demo的目录结构,具体的源码我放到github上面,地址:https://github.com/dufyun/learn-tech-collection/tree/master/redissondemo
注:这里一定要先安装Redis服务,如果没有安装Redis服务,请参考这篇:
如果Redis服务安装到服务器上面,请修改代码中的Redis地址和端口!否则运行不起了!
运行这个类UUidGeneratorLockTest就可以看到效果!测试结果我也在readme.txt进行了总结!
如果在测试和学习的过程中有疑问,可以随时和我联系,也可以加左侧的群或者QQ互相探讨!谢谢!
四:总结
这个时代,信息爆炸,各种技术博文之间互相参考,真正的问题可能没有暴露出来,真正好的文章还是需要鉴别的!(我自己也会参考一些文章,但是我都会进行一些自己验证,一个是为准确性,一个加深自己的对知识的认识)
我也要反思,自己之前也写过一些博文,也是遇到问题了,去网上搜一些解决方案,很多方案确实是理论上可以解决当前遇到的问题,当时去深究,就会发现很多的不完整性!
多思考,多测试!让代码能够更加高效、健壮和安全!
五:参考博文
Redis实现分布式锁全局锁—Redis客户端Redisson中分布式锁RLock实现
分布式锁的几种实现方式