志愿者订单
缓存了志愿者发布的订单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Override public Result queryList() { List<String> shopTypes = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, -1); if (!shopTypes.isEmpty()) { List<ShopType> tmp = shopTypes.stream().map(type -> JSONUtil.toBean(type, ShopType.class)) .collect(Collectors.toList()); return Result.ok(tmp); } List<ShopType> tmp = query().orderByAsc("sort").list(); if (tmp == null){ return Result.fail("店铺类型不存在!!"); } shopTypes = tmp.stream().map(type -> JSONUtil.toJsonStr(type)) .collect(Collectors.toList()); stringRedisTemplate.opsForList().leftPushAll(CACHE_SHOP_TYPE_KEY,shopTypes); return Result.ok(tmp); }
|
如何解决不一致的问题

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class ProductService { private final RedissonClient redissonClient; private final DatabaseService databaseService; private final CacheService cacheService; public ProductService(RedissonClient redissonClient, DatabaseService databaseService, CacheService cacheService) { this.redissonClient = redissonClient; this.databaseService = databaseService; this.cacheService = cacheService; } public void updateProductQuantity(String productId, int newQuantity) { String lockKey = "product:" + productId + ":lock"; RLock lock = redissonClient.getLock(lockKey); try { boolean res = lock.tryLock(10, 10, TimeUnit.MINUTES); if (res) { try { databaseService.updateQuantity(productId, newQuantity); cacheService.updateQuantity(productId, newQuantity); } finally { lock.unlock(); } } else { System.out.println("Could not acquire lock for product: " + productId); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); lock.unlock(); throw new RuntimeException("Interrupted while waiting for lock", e); } } }
|
并发量没那么高,所以我决定采取延迟双删的策略
缓存穿透,采用设置时间2分钟的空值解决
不过我觉得基本上不会怎么用上
也可以采用布隆过滤的方法,但是可能会造成Hash冲突
缓存击穿,采取互斥锁
互斥锁性能会降低,互斥锁只是加在了一个重建缓存上,后面建了缓存之后,他们都不会跑来访问数据库 了
但是采取逻辑过期的话,可能会造成数据不一致,做的和医院有关的系统,不能做这个**
封装了redis工具类,采用函数式编程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) { String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(json)) { return JSONUtil.toBean(json, type); } if (json != null) { return null; } R r = null; String lockKey = LOCK_SHOP_KEY + id; try { boolean flag = tryLock(lockKey); if (!flag) { Thread.sleep(50); return queryWithMutex(keyPrefix, id, type, dbFallback, time, timeUnit); } r = dbFallback.apply(id); if (r == null) { stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } this.set(key, r, time, timeUnit); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { unlock(lockKey); } return r; }
|
用户秒杀优惠券
采用stock作为乐观锁
如果采用stock和之前进行对比的话,同一时间段可能只有一个人可以抢到 ×
于是采用了stock > 0
这个想法 √
实现一人一单
采用synchronized (userId.toString().intern())
分布式系统可能不行, ×
采用分布式redis的setnx锁 + 事务,并且通过UUID防止不同的集群产生同样的线程Id而导致误删。还通过LUA的原子性防止误删 逻辑时间过期本质上就没有锁住了×
采用Redission的锁机制 + 事务 ,锁的可重试获取时间是0.5s,锁的释放时间是10s。这里不涉及到锁重试的,因为本身来说,锁重试一般要调用其他方法,或者回溯才会用到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override public boolean tryLock(long timeoutSec) { String threadId = ID_PREFIX + Thread.currentThread().getId(); Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); }
@Override public void unlock() { String threadId = ID_PREFIX + Thread.currentThread().getId(); String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); if (threadId.equals(id)) { stringRedisTemplate.delete(KEY_PREFIX + name); } }
|
关于spring事务
- 如果是同一个类中
- 父方法带有@Transaction注解
- 子方法带有@Transaction注解,想要让他生效,必须使用代理机制才能生效,或者采用新建bean,类似于不在同一个类调用事务方法一样
- 子方法不带有@Transaction,没有
@Transactional
注解的方法将不会启动新的事务,也不会加入到任何已存在的事务中。它们将在没有事务的上下文中执行。
- 父方法没有带有@Transaction注解
- 子方法带有@Transaction注解,想要让他生效,必须使用代理机制才能生效,或者采用新建bean,类似于不在同一个类调用事务方法一样
- 子方法不带有@Transaction,没有
@Transactional
注解的方法将不会启动新的事务,也不会加入到任何已存在的事务中。它们将在没有事务的上下文中执行
- 如果不在同一个类中,使用传统的代理机制
- 父方法带有@Transaction注解,
- 子方法带有@Transaction注解,根据事务传播行为来说,如果父方法有事务,就加入,如果没有就新建自己独立!
- 子方法不带有@Transaction,没有
@Transactional
注解的方法将不会启动新的事务,也不会加入到任何已存在的事务中。它们将在没有事务的上下文中执行。
- 父方法没有带有@Transaction注解
- 子方法带有@Transaction注解,子方法的事务机制独立,就像普通的Main方法使用事务一样简单。
- 子方法不带有@Transaction,没有
@Transactional
注解的方法将不会启动新的事务,也不会加入到任何已存在的事务中。它们将在没有事务的上下文中执行
也就说,如果一个方法不带有Transaction注解的话,那么无论是什么方法调用它,这个方法都不会运行在事务管理机制中
- 但是以上做法依然有问题,因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用代理来生效,所以这个地方,我们需要获得原始的事务对象, 来操作事务,这里可以使用
AopContext.currentProxy()
来获取当前对象的代理对象,然后再用代理对象调用方法,记得要去IVoucherOrderService
中创建createVoucherOrder
方法
1 2 3 4 5
| Long userId = UserHolder.getUser().getId(); synchronized (userId.toString().intern()) { IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); }
|
1 2 3 4
| <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
|
- 同时在启动类上加上
@EnableAspectJAutoProxy(exposeProxy = true)
注解
1 2 3 4 5 6 7 8 9
| @MapperScan("com.hmdp.mapper") @SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true) public class HmDianPingApplication { public static void main(String[] args) { SpringApplication.run(HmDianPingApplication.class, args); }
}
|
优化了秒杀功能
Stream消息队列,处理异步消息无法确认问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| String queueName = "stream.orders";
private class VoucherOrderHandler implements Runnable {
@Override public void run() { while (true) { try { List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), StreamOffset.create(queueName, ReadOffset.lastConsumed())); if (records == null || records.isEmpty()) { continue; } MapRecord<String, Object, Object> record = records.get(0); Map<Object, Object> values = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true); handleVoucherOrder(voucherOrder); stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId()); } catch (Exception e) { log.error("订单处理异常", e); handlePendingList(); } } } }
private void handlePendingList() { while (true) { try { List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1), StreamOffset.create(queueName, ReadOffset.from("0"))); if (records == null || records.isEmpty()) { break; } MapRecord<String, Object, Object> record = records.get(0); Map<Object, Object> values = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true); handleVoucherOrder(voucherOrder); stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId()); } catch (Exception e) { log.info("处理pending-list异常"); try { Thread.sleep(50); } catch (InterruptedException ex) { throw new RuntimeException(ex); } } } }
|