参考
Redis基础 - 基本类型及常用命令
Redis基础 - Java客户端
Redis 基础 - 短信验证码登录
Redis 基础 - 用Redis查询商户信息
Redis 基础 - 优惠券秒杀《非集群》
synchronized在集群上使用时的问题
synchronized只能够保证一个JVM内部的多个线程之间的互斥,而无法在集群之间互斥,要想解决这个问题必须要使用分布式锁。分布式锁是满足分布式系统或集群模式下“多进程可见”并且能“互斥”的锁。
为何需要分布式锁
比如有两个JVM,JVM1和JVM2,synchronized就是利用JVM内部的锁监视器来控制线程,在JVM的内部因为只有一个锁监视器,所以只会有一个线程获取锁,因此可以实现线程之间的互斥。但当有多个JVM的时候,就会有多个锁监视器,那么就会有多个线程获取到锁,这样的话无法实现多JVM之间的互斥。要想解决这个问题,肯定不能使用JVM内部的锁监视器了,必须让多个JVM去使用同一个锁监视器。所以他一定是在JVM外部的,多JVM进程都可以看到的锁监视器,这时候无论是JVM内部的还是多JVM的线程,都应该去找外部的锁监视器获取锁,这样也就会只有一个线程获取锁,就能实现多进程之间的互斥了。
业务场景
比如JVM1里有线程1在执行业务,她就会去获取互斥锁,她获取锁就会去找外部的锁监视器,一旦获取成功,就在锁监视器里记录当前获取锁的是线程1。此时如果其他线程也来获取锁,比如JVM2内部的线程3,她也会去外部的锁监视器试图获取锁,但因为锁监视器已经有线程1使用着,所以线程3获取一定会失败,失败之后她就会去等待锁释放。一方面,假如JVM1的线程1执行着:先查询订单,若没有就插入新订单,由于她是第一个来的,所以没有订单,所以插入新订单,执行完后就会释放锁。等线程1释放完之后,线程3拿到锁了,她也去执行一样的业务:获取锁成功,查询订单,但查询时由于线程1已经插入了,所以线程3就能查询到订单,由于已经存在,所以直接返回报错。
分布式锁的实现
分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:
MySQL | Redis | zookeeper | |
---|---|---|---|
互斥 | 利用民事权利本身的互斥锁机制互斥 | 利用setnx这样的互斥命令 | 利用节点的唯一性和有序性实现互斥 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开连接,自动释放锁 | 利用锁超时时间,到期释放 | 临时节点,断开连接自动释放 |
基于Redis的分布式锁
实现分布式锁时需要实现的两个基本方法:
(1)获取锁
- 互斥:确保只能有一个线程获取锁,可以利用setnx。为了不让setnx后还没来得及设置expire时恰巧宕机,要保证setnx和expire同时成功或同时失败,所以可以用set命令,set命令有很多参数,比如set key value EX 10 NX,即设置了值还设置了EX(超时时间)参数,为10秒,还配了NX参数(互斥),没值时才设置。利用这个命令可以让NX和EX变成原子操作。
- 非阻塞方式去获取锁:尝试一次,成功返回true,失败返回false
(2)释放锁
- 手动释放:del 锁的key
- 超时释放:获取锁时添加一个超时时间,即expire。避免服务宕机而出现的死锁。
基于Redis实现分布式锁(初级)
ILock.java
public interface ILock{
// 尝试获取锁(因为这里用的是非阻塞获取)
/*
参数:锁持有的超时时间,过期后自动释放
返回值:true 获取锁成功; false 获取锁失败
*/
boolean tryLock(long timeoutSec);
// 释放锁
void unlock();
}
SimpleRedisLock.java
public class SimpleRedisLock implements ILock {
public String name;// 业务名,不同的业务的锁的key要不同(实际上就是key名)。
private StringRedisTemplate stringRedisTemplate;
// 通过构造函数传值
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";//key的前缀
@Override
public boolean tryLock(long timeoutSec) {
// 获取当前线程的id(线程的标识)
long threadId = Thread.currentThread().getId();
// 获取锁,setIfAbsent 如果不存在则执行。value最好加个标识,哪个线程拿到的锁。
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
// 返回值是boolean基本类型,而这里是Boolean,所以直接返回的话会自动拆箱,如果值是null,拆箱时可能会报空指针错误,所以返回时可以这么返回,防止自动拆箱
return Boolean.TURE.equals(success);// Boolean.TURE是常亮,所以与success比较后,一样就返回true,不一样(或是null时)就返回false。
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
代码示例1:用一人一单的例子来测试
VoucherOrderServiceImpl.java
@Resource
prviate ISeckillVoucherService iSeckillVoucherService;
@Resource
prviate RedisIdWorker redisIdWorker;
@Resource
prviate StringRedisTemplate stringRedisTemplate;
@Override
// @Transactional 由于这个方法里只有查的,所以不需要这个。
public Result seckillVoucher(Long voucherId) {
// 1,根