为什么要用分布式锁?
假设一个场景,一个服务里面提供了操作某个变量的接口,在单机的时候,可以使用synchronize或者lock进行加锁防止并发问题,但是假如这个服务有3个实例,每个实例可以操作相应的共享资源,这时候三个请求恰好都分发到不同的实例上去,结果是变量不知道被改成什么样了,也许每个服务的实例里的变量都不一样,那么怎么控制这个变量在面对多个请求时所带来的并发问题呢,这时候需要一个粒度更粗的锁,分布式锁就出来了,可以解决多台机器上的对同一个资源操作的并发问题。
单机redis
命令setnx
setnx等同于set if not exists
设置成功返回1,设置失败返回0
命令:getset
先get后set,先获取指再set
getset key value
这两个指令都具有原子性
加锁的过程主要是通过setnx指令确认是否可以上锁,如果可以则直接成功,不可以则需要则查看是否超时,如果已经超时,为了避免死锁,用getset命令来处理多个线程访问的问题,线程A在上锁以后,线程b想继续操作会发先锁还没超时而退出,具体代码如下
package com.gdut.xg.shop.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* @author lulu
* @Date 2019/7/21 9:10
*/
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate template;
/**
*
* @param key
* @param value 设置时间+超时时间
* @return
*/
public boolean lock(String key,String value){
if(template.opsForValue().setIfAbsent(key,value)){
return true;
}
String currentValue=template.opsForValue().get(key);
//如果锁过期
if(!StringUtils.isEmpty(currentValue)&&Long.parseLong(currentValue)<System.currentTimeMillis()){
//获取上一个锁时间
String oldValue=template.opsForValue().getAndSet(key,value);
//超时即重新上锁
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(value)) {
return true;
}
}
return false;
}
//这里可以用lua脚本保证原子性,下面有相似例子
public void unlock(String key,String value){
try{
String currentValue=template.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
template.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
System.out.println("解锁异常");
}
}
}
可重入锁,redis实现可重入可以使用threalLock+lua脚本实现,其中ThreadLock用于存储当前线程的信息UUID+threadId,用一个map去维护当前线程标志和调用次数,当如果确定是本线程,map的count属性+1,解锁时减一,如果解锁恰好是1并且属于自己的锁就把key删掉,这里有个要注意的点,lua脚本要使用stringRedisTemplate来执行,不然有可能会导致expire的指令执行报错
private static class RedisLockInfoHolder {
static ThreadLocal<String> holder = new ThreadLocal<>();
private static void clear() {
holder.remove();
}
private static String get() {
return holder.get();
}
private static void setValue(String value) {
holder.set(value);
}
}
--一个map,有属性值和调用次数
local isExists = redis.call('exists', KEYS[1])
--代表初始提交
if isExists == 0 then
redis.call('hmset', KEYS[1], 'currentThread', ARGV[1], 'count', 1);
--设置超时时间
redis.call('pexpire', KEYS[1], ARGV[2]);
return true;
--否则判断是否是同一个线程,如果是的话加1,返回true
else
local currentThread = redis.call('hget', KEYS[1], 'currentThread')
--如果当前线程相等,+1返回true
if ARGV[1] == currentThread then
redis.call('hincrby', KEYS[1], 'count', 1)
return true
end
return false;
end
--当为0时候释放锁
local isExists = redis.call('exists', KEYS[1])
--如果不等0,代表有key存在,如果当前count为0,删掉
if isExists == 1 then
local lockInfo = redis.call('hmget', KEYS[1], 'currentThread', 'count')
local currentThread = lockInfo[1]
local count = tonumber(lockInfo[2])
--不是自己的锁,不释放
if currentThread ~= ARGV[1] then
return 0
end
--返回成功解锁
if count == 1 then
redis.call('del', KEYS[1])
return 1
else
--重入锁,仍持有
redis.call('hincrby', KEYS[1], 'count', -1)
return 2
end
else
package com.xl.redisaux.common.utils.lock;
import com.xl.redisaux.common.utils.NamedThreadFactory;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import java.util.Collections;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* @author tanjl11
* @date 2020/10/14 15:07
*/
public class LockUtil {
private StringRedisTemplate lockTemplate;
public LockUtil(StringRedisTemplate lockTemplate) {
this.lockTemplate = lockTemplate;
}
private volatile DefaultRedisScript<Boolean> lockScript;
private volatile DefaultRedisScript<Long> upLockScript;
private volatile DefaultRedisScript<Boolean> extendScript;
private final static long INTERVAL_CHECK_EXPIRE = 30;
private final String PREFIX = "redisLock:";
//每个bucket的大小是1s,一轮有16个
private HashedWheelTimer wheelTimer;
private DefaultRedisScript getExtendScript() {
if (Objects.isNull(extendScript)) {
synchronized (LockUtil.class) {
if (Objects.isNull(extendScript)) {
wheelTimer = new HashedWheelTimer(new NamedThreadFactory("watch-dog-thread-pool",true), 1, TimeUnit.SECONDS, 16);
extendScript = new DefaultRedisScript<>();
extendScript.setResultType(Boolean.class);
extendScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("Extend.lua")));
}
}
}
return extendScript;
}
private DefaultRedisScript getLockScript() {
if (Objects.isNull(lockScript)) {
synchronized (LockUtil.class) {
if (Objects.isNull(lockScript)) {
lockScript = new DefaultRedisScript();
lockScript.setResultType(Boolean.class);
lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("Lock.lua")));
}
}
}
return lockScript;
}
private DefaultRedisScript getUnLockScript() {
if (Objects.isNull(upLockScript)) {
synchronized (LockUtil.class) {
if (Objects.isNull(upLockScript)) {
upLockScript = new DefaultRedisScript();
upLockScript.setResultType(Long.class);
upLockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("UnLock.lua")));
}
}
}
return upLockScript;
}
/**
* @param key
* @param time
* @param awaitTime
* @param retryCount
* @param sleepTime
* @return
*/
public Boolean tryLockInSeconds(String key, long time, long awaitTime, long retryCount, long sleepTime) {
return tryLockInSeconds(key, time, TimeUnit.SECONDS, awaitTime, TimeUnit.SECONDS, retryCount, sleepTime, TimeUnit.SECONDS);
}
/**
* 超时可重入锁
*
* @param key 锁名
* @param expireTime 锁过期时间
* @param unit 锁过期时间单位
* @param awaitTime 锁等待超时时间
* @param awaitUnit 锁
* @param retryCount 最大获取次数
* @param sleepTime 每次获取失败后的睡眠时间
* @param sleepUnit 睡眠单位
* @return
*/
public Boolean tryLockInSeconds(String key, long expireTime, TimeUnit unit, long awaitTime, TimeUnit awaitUnit, long retryCount, long sleepTime, TimeUnit sleepUnit) {
//先获取一次
Boolean isLock = tryLock(key, expireTime, unit);
//不行再获取
if (!isLock) {
long nanos = awaitUnit.toNanos(awaitTime);
final long deadline = System.nanoTime() + nanos;
int count = 0;
while (true) {
nanos = deadline - System.nanoTime();
//超时
if (nanos <= 0L) {
return false;
}
isLock = tryLock(key, expireTime, unit);
if (isLock) {
return true;
}
//如果大于最大获取次数或者线程被中断
if (count++ > retryCount || Thread.interrupted()) {
return false;
}
//阻塞
LockSupport.parkNanos(sleepUnit.toNanos(sleepTime));
}
}
return true;
}
/**
* 无限延长方法
*
* @param key
* @return
*/
public Boolean tryLock(String key) {
//先锁一次
Boolean lock = tryLock(key, INTERVAL_CHECK_EXPIRE, TimeUnit.SECONDS);
if (lock) {
DefaultRedisScript extendScript = getExtendScript();
wheelTimer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//延迟0.8后开始校验,是否存在锁,存在就延期,不存在就删除,
Boolean isExtend = (Boolean) lockTemplate.execute(extendScript, Collections.singletonList(PREFIX + key), String.valueOf(TimeUnit.MILLISECONDS.convert(INTERVAL_CHECK_EXPIRE, TimeUnit.SECONDS)));
if (isExtend) {
wheelTimer.newTimeout(this, (long) (INTERVAL_CHECK_EXPIRE * 0.8), TimeUnit.SECONDS);
} else {
timeout.cancel();
}
}
}, (long) (INTERVAL_CHECK_EXPIRE * 0.8), TimeUnit.SECONDS);
return true;
}
return false;
}
/**
* 按照秒来锁
*
* @param key
* @param time
* @return
*/
public Boolean tryLock(String key, long time) {
return tryLock(key, time, TimeUnit.SECONDS);
}
/**
* 哨兵模式如果保证绝对可靠需要redLock算法支持,redission已有实现
*/
public Boolean tryLock(String key, long expireTime, TimeUnit unit) {
key = PREFIX + key;
String threadSign = RedisLockInfoHolder.get();
//设置线程标志
if (Objects.isNull(threadSign)) {
String uuid = UUID.randomUUID().toString();
String value = uuid + Thread.currentThread().getId();
threadSign = value;
RedisLockInfoHolder.setValue(threadSign);
}
long millis = unit.toMillis(expireTime);
//要使用stringRedisTemplate才可以设置上
Boolean isLock = (Boolean) lockTemplate.execute(getLockScript(), Collections.singletonList(key), threadSign, String.valueOf(millis));
return isLock;
}
/**
* 解锁
*
* @param key
* @return
*/
public UnLockStatus unLock(String key) {
key = PREFIX + key;
String threadSign = RedisLockInfoHolder.get();
Long result = (Long) lockTemplate.execute(getUnLockScript(), Collections.singletonList(key), threadSign);
//如果成功解锁,清楚线程标志
if (Objects.equals(result, UnLockStatus.UNLOCK_SUCCESS.getStatus())) {
RedisLockInfoHolder.clear();
}
return UnLockStatus.getByStatus(result);
}
private static class RedisLockInfoHolder {
static ThreadLocal<String> holder = new ThreadLocal<>();
private static void clear() {
holder.remove();
}
private static String get() {
return holder.get();
}
private static void setValue(String value) {
holder.set(value);
}
}
}
public enum UnLockStatus {
LOCK_NOT_EXISTS(-1L, "该锁不存在"),
NOT_HAVE_LOCK(0L, "该锁非调用线程所占用"),
UNLOCK_SUCCESS(1L, "解锁成功"),
RETAIN_LOCK(2L, "解锁但仍持有");
private Long status;
private String mean;
UnLockStatus(Long status, String mean) {
this.status = status;
this.mean = mean;
}
public Long getStatus() {
return status;
}
public String getMean() {
return mean;
}
public static UnLockStatus getByStatus(Long status){
for (UnLockStatus value : values()) {
if(Objects.equals(value.getStatus(),status)){
return value;
}
}
return null;
}
}
续约功能实现思路:
默认维护一个过期时间,由一个定时任务去维护,等差不多过期了,就去检查一下,查看键是否存在,不存在把定时任务取消
具体可以使用netty的时间轮结合lua脚本实现
local isExists = redis.call('exists', KEYS[1])
if isExists == 0 then
return false
else
redis.call('pexpire', KEYS[1], ARGV[1]);
return true
end
时间轮使用,判断如果还有键,就再注册一个任务,关于时间轮的分析网上也有很多,这里不做介绍,注意时间轮的运行线程需设置为守护线程
public Boolean tryLock(String key) {
//先锁一次
Boolean lock = tryLock(key, INTERVAL_CHECK_EXPIRE, TimeUnit.SECONDS);
if (lock) {
DefaultRedisScript extendScript = getExtendScript();
wheelTimer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//延迟0.8的时间后开始校验,是否存在锁,存在就延期,不存在取消任务执行
Boolean isExtend = (Boolean) lockTemplate.execute(extendScript, Collections.singletonList(PREFIX + key), String.valueOf(TimeUnit.MILLISECONDS.convert(INTERVAL_CHECK_EXPIRE, TimeUnit.SECONDS)));
if (isExtend) {
wheelTimer.newTimeout(this, (long) (INTERVAL_CHECK_EXPIRE * 0.8), TimeUnit.SECONDS);
} else {
timeout.cancel();
}
}
}, (long) (INTERVAL_CHECK_EXPIRE * 0.8), TimeUnit.SECONDS);
return true;
}
return false;
}
集群:
引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redission.version}</version>
</dependency>
编写配置文件
import lombok.Setter;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
/**
* @author: lele
* @date: 2019/9/23 下午11:15
*
*/
@Configuration
@Setter
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private Integer port;
@Value("${spring.redis.password}")
private String password;
@Bean(destroyMethod = "shutdown")
public RedissonClient redission() {
Config config = new Config();
//配置还有主从、集群、哨兵、该例子为单实例
config.useSingleServer().setDatabase(0).
setAddress( host + ":" + port).
setPassword(password);
return Redisson.create(config);
}
}
封装RedissionLock,redission里面还有更多的API,详情请看官方文档
import org.redisson.api.*;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author: lele
* @date: 2019/9/23 下午7:15
* 使用方式:
* 注入
* ReadWriteLock lock=redissionLock.getReadWriteLock(lockName);
* lock.lock()
* 业务逻辑
* lock.unlock()
*/
@Component
public class RedissionLock {
@Resource
private RedissonClient redisson;
/**
* 获取字符串对象
*
* @param objectName
* @return
*/
public <T> RBucket<T> getRBucket(String objectName) {
RBucket<T> bucket = redisson.getBucket(objectName);
return bucket;
}
/**
* 获取Map对象
*
* @param objectName
* @return
*/
public <K, V> RMap<K, V> getRMap(String objectName) {
RMap<K, V> map = redisson.getMap(objectName);
return map;
}
/**
* 获取有序集合
*
* @param objectName
* @return
*/
public <V> RSortedSet<V> getRSortedSet(String objectName) {
RSortedSet<V> sortedSet = redisson.getSortedSet(objectName);
return sortedSet;
}
/**
* 获取集合
*
* @param objectName
* @return
*/
public <V> RSet<V> getRSet(String objectName) {
RSet<V> rSet = redisson.getSet(objectName);
return rSet;
}
/**
* 获取列表
*
* @param objectName
* @return
*/
public <V> RList<V> getRList(String objectName) {
RList<V> rList = redisson.getList(objectName);
return rList;
}
/**
* 获取队列
*
* @param objectName
* @return
*/
public <V> RQueue<V> getRQueue(String objectName) {
RQueue<V> rQueue = redisson.getQueue(objectName);
return rQueue;
}
/**
* 获取双端队列
*
* @param objectName
* @return
*/
public <V> RDeque<V> getRDeque(String objectName) {
RDeque<V> rDeque = redisson.getDeque(objectName);
return rDeque;
}
/*
*获取阻塞队列
* @param redisson
* @param objectName
* @return
*/
public <V> RBlockingQueue<V> getRBlockingQueue(String objectName) {
RBlockingQueue rb = redisson.getBlockingQueue(objectName);
return rb;
}
/**
* 获取锁
*
* @param objectName
* @return
*/
public RLock getRLock(String objectName) {
RLock rLock = redisson.getLock(objectName);
return rLock;
}
/**
* 公平锁
*
* @param objectName
* @return
*/
public RLock getFairLock(String objectName) {
RLock rLock = redisson.getFairLock(objectName);
return rLock;
}
/**
* 读写锁
*
* @param objectName
* @return
*/
public RReadWriteLock getReadWriteLock(String objectName) {
RReadWriteLock readWriteLock = redisson.getReadWriteLock(objectName);
return readWriteLock;
}
/**
* 获取原子数
*
* @param objectName
* @return
*/
public RAtomicLong getRAtomicLong(String objectName) {
RAtomicLong rAtomicLong = redisson.getAtomicLong(objectName);
return rAtomicLong;
}
/**
* 获取记数锁
*
* @param objectName
* @return
*/
public RCountDownLatch getRCountDownLatch(String objectName) {
RCountDownLatch rCountDownLatch = redisson
.getCountDownLatch(objectName);
return rCountDownLatch;
}
/**
* 获取消息的Topic
*
* @param objectName
* @return
*/
public <M> RTopic<M> getRTopic(String objectName) {
RTopic<M> rTopic = redisson.getTopic(objectName);
return rTopic;
}
}
测试,user表里面有个积分的字段,初始设为10000,启动三个实例,然后用jmeter进行测试
package com.imooc.alicloud.usercenter.web;
import com.imooc.alicloud.usercenter.dao.user.UserMapper;
import com.imooc.alicloud.usercenter.domain.entity.user.User;
import com.imooc.alicloud.usercenter.lock.RedissionLock;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
* @author lele
* @since 2019-09-21
*/
@RestController
@RequestMapping("/channel")
public class TestController {
@Autowired
private UserMapper userMapper;
@Autowired
private RedissionLock redissionLock;
private String name = "i";
@GetMapping("/test/reduce")
public void get() {
RLock rLock = redissionLock.getRLock(name);
rLock.lock();
User user = userMapper.selectById(1);
user.setBonus(user.getBonus() - 1);
userMapper.updateById(user);
rLock.unlock();
}
}
结果确实减少了1500
把锁去掉再测试一次,很快就执行完了,但积分没有像预期一样减少相应的分数
缓存常用注解:
@EnableCaching用在程序入口开启缓存
@Cacheable(查询操作使用,有缓存则走缓存)
当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的,如果用于对象的话要进行序列化
key:默认是方法参数,动态 :#参数名,#参数index
cacheNames/value:前缀
condition:符合条件才缓存
unless:#result代表结果对象
@Cacheable(cacheNames = "productData",key = "#categoryId",unless="#result.length()!=0")
public ProductDataVO getProductData(Integer categoryId) {xxx}
存储的key为productData::categoryId,value是对象
@CachePut 每次都会走一遍流程,再把结果更新到缓存(对于插入、修改、删除方法可以使用)
@CacheEvict(清除缓存)
@Caching 操作组合
package org.springframework.cache.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
参考配置
package com.trendy.center.channel.common.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Setter;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* 定义key生成器,可在注解上面使用
* @return
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (Object target, Method method, Object... params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return new RedisCacheManager(
RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
this.redisCacheConfigurationWithTtl(30), // 默认策略,未配置的 key 会使用这个
this.getRedisCacheConfigurationMap() // 指定 key 策略
);
}
//自定义key策略,value可设置不同的configuration
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
/* redisCacheConfigurationMap.put("cachelist", this.redisCacheConfigurationWithTtl(3000));*/
return redisCacheConfigurationMap;
}
private RedisCacheConfiguration redisCacheConfigurationWithTtl(Integer minutes) {
//代替默认的jdk序列化对象,序列化方便查看value
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//缓存配置
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer)
).entryTtl(Duration.ofMinutes(minutes));
return redisCacheConfiguration;
}
}
同步机制:
全同步过程
slave发送sync命令到master
master启动一个后台进程,将redis中的数据快照保存到文件中
master将保存数据快照期间接受的写命令缓存起来
master完成写操作文件后,将该文件发送到slave
使用新的rdb文件替换掉旧的rdb文件
master将这期间收集的增量命令发送给slave
增量同步过程
master接受到用户的操作指令,判断是否传播到slave
将操作记录追加到aof文件
将操作传播到其他slave:1.对齐主从库;2.往相应缓存写入指令
将缓存中的数据发给slave
解决主从模式下master宕机 哨兵 redis sentinel
监控:检查主从服务器是否运行正常
提醒:通过api向管理员或其他应用程序发送通知
自动故障迁移:主从切换