1 回顾下单机JVM锁
1.1 为什么要引入锁?
因为,线程是进程的一个实体,同一进程下的多个线程可以进行资源的共享
,多个线程共享一个资源时则会进行资源的竞争进而引发线程异常。
无锁模式下资源不同步的体现:
代码:
public class Test1 { Ticket ticket = new Ticket(); private void sellTest() { ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingDeque<>()); for (int i = 0; i < 60; i++) { executor.execute(() -> { ticket.sellNoLock(); }); } executor.shutdown(); } public static void main(String[] args) { Test1 test1 = new Test1(); test1.sellTest(); } } class Ticket { private int count = 50; //---------------------不加锁----------------------- /** * 方法需要sleep,不然太过简单不会导致多线程问题 */ public void sellNoLock() { if (count > 0) { try { Thread.sleep(100); System.out.println(Thread.currentThread().getId() + "--卖出了" + (count--) + "--剩余票数:" + count); } catch (InterruptedException e) { e.printStackTrace(); } } } }
结果:
1.2 单机JVM实现锁的两种方式
1.2.1 互斥锁
//-------------------------synchronized互斥锁--------------------- /** * synchronized互斥锁 */ public synchronized void sellSynchronizedLock() { if (count > 0) { try { Thread.sleep(100); System.out.println(Thread.currentThread().getId() + "--卖出了" + (count--) + "--剩余票数:" + count); } catch (InterruptedException e) { e.printStackTrace(); } } }
1.2.2 lock可重入锁
//---------------------------ReentrantLock可重入锁--------------- ReentrantLock lock = new ReentrantLock(); /** *ReentrantLock可重入锁 */ public void sellReentrantLock() { lock.lock(); try { Thread.sleep(100); if (count > 0) { System.out.println(Thread.currentThread().getId() + "--卖出了" + (count--) + "--剩余票数:" + count); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }
以上两种都可以对线程进行对资源的互斥访问,可以这样理解
因此结果就是:
2 Redis实现分布式锁
2.1 为什么要引入分布式锁?
因为集群环境下,无法避免要把一个项目部署成多个节点,但是数据的一致性导致每个节点访问的数据都是一样的,至此我们可以把每一个项目节点都当做一个线程,整个分布式集群当做一个进程,数据就是多个节点共享的资源,因此难免会引发分布式环境下的多线程问题。
2.2 经典redis分布式锁
redis实现分布式锁需要借助lua脚本,所谓lua,解释起来比较麻烦,推荐看下Lua语言
简单的来讲,redis实现分布式锁利用的就是redis的特有命令加上高速存取的特性来进行的。
class RedisLock { /** * 锁键 */ private String lock_key = "redis_lock"; /** * 锁过期时间 */ protected long internalLockLeaseTime = 10000; /** * 获取锁的超时时间 */ private long timeout = 999999; /** * SET命令的参数 */ SetParams params = SetParams.setParams().nx().px(internalLockLeaseTime); private JedisPool jedisPool = new JedisPool(); /** * 加锁 * * @param id * @return */ public boolean lock(String id) { Jedis jedis = jedisPool.getResource(); Long start = System.currentTimeMillis(); try { for (; ; ) { //SET命令返回OK ,则证明获取锁成功 String lock = jedis.set(lock_key, id, params); if ("OK".equals(lock)) { return true; } //否则循环等待,在timeout时间内仍未获取到锁,则获取失败 long l = System.currentTimeMillis() - start; if (l >= timeout) { return false; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } finally { jedis.close(); } } /** * 解锁 * * @param id * @return */ public boolean unlock(String id) { Jedis jedis = jedisPool.getResource(); String script = "if redis.call('get',KEYS[1]) == ARGV[1] then" + " return redis.call('del',KEYS[1]) " + "else" + " return 0 " + "end"; try { Object result = jedis.eval(script, Collections.singletonList(lock_key), Collections.singletonList(id)); if ("1".equals(result.toString())) { return true; } return false; } finally { jedis.close(); } } }
使用:
//--------------------------redis分布式锁-------------------------- RedisLock redisLock = new RedisLock(); /** * redis分布式锁 */ public void sellRedisLock() { String uid = UUID.randomUUID().toString(); redisLock.lock(uid); try { Thread.sleep(100); if (count > 0) { System.out.println(Thread.currentThread().getId() + "--卖出了" + (count--) + "--剩余票数:" + count); } } catch (InterruptedException e) { e.printStackTrace(); } finally { redisLock.unlock(uid); } }
2.3 redis实现可重入分布式锁
使用方式:
//---------------------redis分布式可重入锁----------------------- Config config = null; RedissonClient client = null; RLock rLock = null; public Ticket() { config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); client = Redisson.create(config); rLock = client.getLock("lock1"); } /** * redis分布式可重入锁 */ public void sellRedissonLock() { try { rLock.lock(); Thread.sleep(100); if (count > 0) { System.out.println(Thread.currentThread().getId() + "--卖出了" + (count--) + "--剩余票数:" + count); } } catch (Exception e) { e.printStackTrace(); } finally { rLock.unlock(); } }
2.4 流程和原理
Lua脚本原理:
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
解释:
- KEYS[1]代表的是你加锁的那个key,比如说:
- ARGV[1]代表的就是锁key的默认生存时间,默认30秒。
3 分布式锁的其他实现
3.1 基于数据库的分布式锁
要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们想要获得锁的时候,就可以在该表中增加一条记录,想要释放锁的时候就删除这条记录。
典型应用就是MySQL的乐观锁和悲观锁。
具体请参考下这篇文章:https://honeypps.com/architect/distribute-lock-based-on-database/